Assembleur

Langage assembleur Linux/Unix et Intel x86

Un langage d'assemblage ou langage assembleur est, en programmation informatique, un langage de bas niveau qui représente le langage machine sous une forme lisible par un humain. Les combinaisons de bits du langage machine sont représentées par des symboles dits « mnémoniques » (du grec mnêmonikos, relatif à la mémoire), c'est-à-dire faciles à retenir. Le programme assembleur convertit ces mnémoniques en langage machine en vue de créer par exemple un fichier objet ou un fichier exécutable.

Dans la pratique courante, le même terme assembleur est utilisé à la fois pour désigner le langage d'assemblage et le programme assembleur qui le traduit. On parle ainsi de « programmation en assembleur ».

Sommaire

  • 1 Histoire
  • 2 Particularités de l'assembleur
    • 2.1 Un langage spécifique à chaque processeur
    • 2.2 Désassemblage
    • 2.3 Instructions machine
    • 2.4 Directives du langage assembleur
  • 3 Exemples simples
    • 3.1 Afficher Bonjour
    • 3.2 Lire le clavier (16 caractères max) puis l'afficher
  • 4 Exemples simples, syntaxe Intel x86
    • 4.1 Afficher Bonjour
    • 4.2 Lire le clavier (64 caractères max) puis l'afficher
  • 5 Usage du langage assembleur
  • 6 Macro-assembleur
    • 6.1 Pseudo-instructions
  • 7 Programmation structurée en assembleur
  • 8 Notes et références
  • 9 Voir aussi
    • 9.1 Articles connexes
    • 9.2 Liens externes

Histoire

Carte formatée pour être perforée pour l'assembleur de l'IBM 1620. La carte fait apparaître plusieurs zones de colonne pour correctement séparer le numéro de ligne, l'étiquette ou label, l'instruction et les opérandes.

Les programmes de l'EDSAC (1949), premier calculateur à programmes enregistrés, étaient rédigés en utilisant des mnémoniques alphabétiques d'une lettre pour chaque instruction. La traduction était alors faite à la main par les programmeurs, une opération longue, fastidieuse et entachée d'erreurs.

Le premier programme assembleur a été écrit par Nathaniel Rochester pour l'IBM 701 (le premier ordinateur commercialisé par IBM) en 1954.

Les langages d'assemblages ont éliminé une grande partie des erreurs commises par les programmeurs de la première génération d'ordinateurs, en les dispensant de mémoriser les codes numériques des instructions et de faire des calculs d'adresses. La programmation en assembleur était alors utilisée pour écrire toutes sortes de programmes.

Dans les années 1970-80, l'utilisation de l'assembleur pour écrire des applications a été très largement supplantée par l'emploi de langages de programmation de haut niveau : FORTRAN, COBOL, PL/I, etc.

Les systèmes d'exploitations ont été écrits en langage d'assemblage jusqu'à l'introduction de MCP de Burroughs, en 1961, qui était écrit en ESPOL, un dialecte d'Algol.

L'assembleur est revenu quelque peu en faveur sur les premiers micro-ordinateurs, où les caractéristiques techniques (taille mémoire réduite, puissance de calcul faible, architecture spécifique de la mémoire…) imposaient de fortes contraintes, auxquelles s'ajoute un facteur psychologique important, l'attitude « hobbyiste » des premiers utilisateurs de micro-ordinateurs, qui ne se satisfaisaient pas de la lenteur des programmes écrits avec le BASIC interprété généralement fourni avec l'ordinateur.

Des gros programmes ont été écrits entièrement en assembleur pour les micro-ordinateurs, comme le système d'exploitation DOS de l'IBM PC (environ 4000 lignes de code), et la tableur Lotus 1-2-3 (son rival Multiplan, qui existait déjà sous CP/M, était écrit en C). Dans les années 1990, c'était aussi le cas pour la plupart des jeux pour consoles vidéo.

Particularités de l'assembleur

Un langage spécifique à chaque processeur

Le langage machine est le seul langage qu'un processeur puisse exécuter. Or chaque famille de processeurs utilise un jeu d'instructions différent.

Par exemple, un processeur de la famille x86 reconnaît une instruction du type

10110000 01100001

En langage assembleur, cette instruction est représentée par un équivalent plus facile à comprendre pour le programmeur :

  movb $0x61,%al

(10110000 = movb %al
01100001 = $0x61)

Ce qui signifie : « écrire le nombre 97 (la valeur est donnée en hexadécimal : 6116 = 9710) dans le registre AL ».

Ainsi, le langage assembleur, représentation exacte du langage machine, est spécifique à chaque architecture de processeur. De plus, plusieurs groupes de mnémoniques ou de syntaxes de langage assembleur peuvent exister pour un seul ensemble d'instructions, créant ainsi des macro-instructions.

Désassemblage

La transformation du code assembleur en langage machine est accomplie par un programme nommé assembleur. L'opération inverse, à savoir retrouver l'assembleur équivalent à un morceau de code machine, porte un nom : il s'agit de désassemblage.

Contrairement à ce que l'on pourrait penser, il n'y a pas toujours de correspondance un à un (une bijection) entre le code assembleur et le langage machine. Sur certains processeurs, le désassemblage peut donc donner un code dont la compréhension est très difficile pour un humain tout en restant parfaitement compilable par un ordinateur. L'impossibilité d'un désassemblage peut avoir diverses raisons : usage de code auto-modifiant, instructions de taille variables, impossibilité de faire la différence entre code et données, etc. (Code impénétrable)

Qui plus est, de nombreux éléments présents dans un code assembleur sont perdus lors de sa traduction en langage machine. Lors de la création du code en assembleur, le programmeur peut affecter des noms aux positions en mémoire, commenter son code, utiliser des macro-instructions ou utiliser du code généré sous conditions au moment de l'assemblage. Tous ces éléments sont réduits lors de l’assemblage à ce qui est strictement nécessaire pour la machine et n'apparaissent donc pas clairement lors du désassemblage : par exemple, une position en mémoire n’est repérée que par son adresse numérique ou par un offset.

Instructions machine

Certaines opérations fondamentales sont disponibles dans la plupart des jeux d'instructions.

  • déplacement dans la mémoire :
    • chargement d'une valeur dans un registre ;
    • déplacement d'une valeur depuis un emplacement mémoire dans un registre, et inversement ;
  • calcul :
    • addition, ou soustraction des valeurs de deux registres et chargement du résultat dans un registre ;
    • combinaison de valeurs de deux registres suivant une opération booléenne (ou opération bit à bit) ;
  • modification du déroulement du programme :
    • saut à un autre emplacement dans le programme (normalement, les instructions sont exécutées séquentiellement, les unes après les autres) ;
    • saut à un autre emplacement, mais après avoir sauvegardé l'instruction suivante afin de pouvoir y revenir (point de retour) ;
    • retour au dernier point de retour ;
  • comparaison :
    • comparer les valeurs de deux registres.

Et on trouve des instructions spécifiques avec une ou quelques instructions pour des opérations qui auraient dû en prendre beaucoup. Exemples :

  • déplacement de grands blocs de mémoire ;
  • multiplication, division ;
  • arithmétique lourde (sinus, cosinus, racine carrée, opérations sur des vecteurs) ;
  • application d'une opération simple (par exemple, une addition) à un ensemble de données par l'intermédiaire des extensions MMX ou SSE des nouveaux processeurs.

Directives du langage assembleur

En plus de coder les instructions machine, les langages assembleur ont des directives supplémentaires pour assembler des blocs de données et affecter des adresses aux instructions en définissant des étiquettes ou labels.

Ils sont capables de définir des expressions symboliques qui sont évaluées à chaque assemblage, rendant le code encore plus facile à lire et à comprendre.

Ils ont habituellement un langage macro intégré pour faciliter la génération de codes ou de blocs de données complexes.

Exemples simples

Voici quelques exemples simples :

  • en syntaxe AT&T (écrits pour l'assembleur GNU (GAS) pour Linux) ;
  • utilisant le jeu d'instructions i386 ;
  • à utiliser comme suit :

$ gcc foo.S -c -o foo.o
$ ld foo.o -o foo
$ ./foo

Afficher Bonjour

(les commentaires se trouvent après les points-virgule)

str:
.ascii "Bonjour\n"
.global _start

_start:
movl $4, %eax
movl $1, %ebx
movl $str, %ecx
movl $8, %edx
int $0x80
movl $1, %eax
movl $0, %ebx
int $0x80

;Compilation:
;as code.s -o code.o
;ld code.o -o code

;Execution:
;./code

Lire le clavier (16 caractères max) puis l'afficher

# define N 16

.global _start

.comm BUFF , N

_start: mov $3 , %eax
mov $0 , %ebx
mov $BUFF , %ecx
mov $N , %edx
int $0x80

mov %eax , %edx
mov $4 , %eax
mov $1 , %ebx
mov $BUFF , %ecx
int $0x80

mov $1 , %eax
mov $0 , %ebx
int $0x80

Exemples simples, syntaxe Intel x86

Voici les mêmes exemples, avec quelques différences :

  • en syntaxe Intel x86, écrit pour l'assembleur NASM ;
  • utilisant le jeu d'instructions i386 ;
  • à utiliser comme suit:

$ nasm -f elf foo.asm
$ ld foo.o -o foo
$ ./foo

Afficher Bonjour

(les commentaires se trouvent après les points-virgule)

section .data ; Variables initialisées
Buffer: db 'Bonjour', 10 ; En ascii, 10 = '\n'. La virgule sert à concaténer les chaines
BufferSize: equ $-Buffer ; Taille de la chaine

section .text ; Le code source est écrit dans cette section
global _start ; Définition de l'entrée du programme

_start: ; Entrée du programme

mov eax, 4 ; Appel de sys_write
mov ebx, 1 ; Sortie standard STDOUT
mov ecx, Buffer ; Chaine à afficher
mov edx, BufferSize ; Taille de la chaine
int 80h ; Interruption du kernel

mov eax, 1 ; Appel de sys_exit
mov ebx, 0 ; Code de retour
int 80h ; Interruption du kernel

Lire le clavier (64 caractères max) puis l'afficher

section .bss ; Section des variables non-initialisées
Buffer: resb 64 ; Réservation de 64 blocs (octets ?) mémoire pour la variable où sera stockée l'entrée de l'utilisateur
BufferSize: equ $-Buffer ; taille de cette variable

section .text ; Section du code source
global _start

_start: ; Entrée du programme

mov eax, 3 ; Appel de sys_read
mov ebx, 0 ; Entrée standard STDIN
mov ecx, Buffer ; Stockage de l'entrée de l'utilisateur
mov edx, BufferSize ; Taille maximale
int 80h ; Interruption du kernel

mov eax, 4 ; Appel de sys_write
mov ebx, 1 ; Sortie standard STDOUT
mov ecx, Buffer ; Chaine à afficher
mov edx, BufferSize ; Taille de la chaine
int 80h ; Interruption du kernel

mov eax, 1 ; Appel de sys_exit
mov ebx, 0 ; Code de retour
int 80h ; Interruption du kernel

Usage du langage assembleur

Il y a des débats sur l'utilité du langage assembleur. Dans beaucoup de cas, des compilateurs -optimiseurs

peuvent transformer du langage de haut niveau en un code qui tourne aussi efficacement qu'un code assembleur écrit à la main par un très bon programmeur, tout en restant beaucoup plus facile, rapide (et donc moins coûteux) à écrire, à lire et à maintenir.

L'efficacité était déjà une préoccupation dans les années 1950, on en trouve trace dans le manuel du langage FORTRAN (sorti en 1956) pour l'ordinateur IBM 704:

Object programs produced by Fortran will be nearly as efficient as those written by good programmers.

Les compilateurs ayant entre-temps fait d'énormes progrès, il est donc évident que l'immense majorité des programmes sont maintenant écrits en langages de haut niveau pour des raisons économiques, le surcoût de programmation l'emportant très largement sur le gain résultant de l'amélioration espérée des performances.

Cependant, il reste quelques cas très spécifiques où l'utilisation de l'assembleur se justifie encore :

  1. quelques calculs complexes écrits directement en assembleur, en particulier sur des machines massivement parallèles, seront plus rapides, les compilateurs n'étant pas assez évolués pour tirer parti des spécificités de ces architectures ;
  2. certaines routines (drivers) sont parfois plus simples à écrire en langage de bas niveau ;
  3. des tâches très dépendantes du système, exécutées dans l'espace mémoire du système d'exploitation sont parfois difficiles, voire impossibles à écrire dans un langage de haut niveau. Par exemple, les instructions assembleur qui permettent à Windows de gérer le changement de tâche (LGDT et LLDT) sur microprocesseur i386 et suivants ne peuvent pas être émulées ou générées par un langage évolué. Il faut nécessairement les coder dans un court sous-programme assembleur qui sera appelé à partir d'un programme écrit en langage évolué.

Certains compilateurs transforment, lorsque leur option d'optimisation la plus haute n'est pas activée, des programmes écrits en langage de haut niveau en code assembleur, chaque instruction de haut niveau se traduisant en une série d'instructions assembleur rigoureusement équivalentes et utilisant les mêmes symboles ; cela permet de voir le code dans une optique de débogage et de profilage, ce qui permet de gagner parfois beaucoup plus de temps en remaniant un algorithme. En aucun cas ces techniques ne peuvent être conservées pour l'optimisation finale.

La programmation des systèmes embarqués, souvent à base de microcontrôleurs, est une « niche » traditionnelle pour la programmation en assembleur. En effet ces systèmes sont souvent très limités en ressources (par exemple un microcontrôleur PIC 16F84 est limité à 1024 instructions de 14 bits, et sa mémoire vive contient 136 octets) et requièrent donc une programmation de bas-niveau très optimisée pour en exploiter les possibilités. Toutefois, l'évolution du matériel fait que les composants de ces systèmes deviennent de plus en plus puissants à un coût et à une consommation électrique constants, l'investissement dans une programmation « tout assembleur » beaucoup plus coûteuse en heures de travail devient alors un non-sens en termes d'efforts. Typiquement, la programmation en assembleur est beaucoup plus longue, plus délicate (car le programmeur doit prendre en compte tous les micro-détails du développement dont il s'abstient en langage évolué) et donc considérablement plus coûteuse que la programmation en langage de haut niveau. Il ne faut donc la réserver qu'aux situations pour lesquelles on ne peut pas faire autrement.

Macro-assembleur

Beaucoup d'assembleurs gèrent un langage de macros. Il s'agit de regrouper plusieurs instructions afin d'avoir un enchaînement plus logique et moins fastidieux.
Par exemple (en assembleur Microsoft MASM) :

putchar Macro car ; Prototype de la macro
ifdef car ; si car est défini
mov dl,car ; le mettre dans dl
endif
mov ah,2 ; ah=2 : fonction "putchar" en DOS
int 21h ; appel au DOS
endm ; fin macro

est une macro qui affiche un caractère sous MS-DOS. On l'utilisera par exemple ainsi :

        putchar "X"

Et cela générera :

mov dl,"X"
mov ah,2
int 21h

Pseudo-instructions

Une pseudo-instruction est un type particulier de macro-instruction. Elle est prédéfinie par l'éditeur du logiciel assembleur et sa fonction est d'émuler une instruction manquante du processeur ou de faciliter l'usage d'une instruction existante. Comme la pseudo-instruction a un nom très ressemblant à celui d'une vraie instruction du processeur, il est possible à première vue de la confondre avec une de ces dernières. Par exemple, un processeur RISC peut ne pas posséder d'instruction JMP, instruction permettant de sauter à un point particulier du programme et de continuer son exécution en séquence. L'éditeur du logiciel aura dans ce cas créé à l'intention du programmeur une pseudo-instruction « JMP <paramètre> », qui sera remplacée à l'assemblage par une instruction « mov pc, <paramètre> », pc étant le pointeur de l'instruction sur le point d'être exécutée. Autre exemple, une pseudo-instruction « PUSH <paramètre> » sera remplacée par un stockage de <paramètre> à l'adresse pointée par sp avec pré-décrémentation de celui-ci, sp étant le pointeur de pile du processeur.

Sur des microprocesseurs ou microcontroleurs RISC tels que ceux de la famille ARM, il n'existe pas d'instruction assembleur permettant de charger n'importe quelle constante immédiate dans un registre, quelle que soit sa valeur. La plupart des assembleurs disposent d'une pseudo-instruction permettant un tel chargement de la façon la plus efficace possible en termes de temps d'exécution, épargnant cette tâche au programmeur.

Programmation structurée en assembleur

Support pour programmation structurée : quelques éléments de programmation structurée ont été intégrés pour encoder le flux d'exécution par le docteur H. D. Mills (mars 1970), et mis en œuvre par Marvin Kessler qui a étendu l'assembleur S/360 macro avec if / else / endif et même des blocs de contrôle de flux. Cela a été un moyen de réduire ou d'éliminer l'utilisation des opérations de GOTO dans le code assembleur.

Notes et références

Voir aussi

Articles connexes

  • Programme assembleur

Liens externes

Sur les autres projets Wikimedia :

· Assembleur, sur Wikimedia Commons

· assembleur, sur le Wiktionnaire

· Assembleur, sur Wikiversity

· Assembleur, sur Wikibooks

  • (fr) Initiation à la programmation en assembleur 64 bits Cours et exercices corrigés avec GNU/linux, par P. Jourlin


HxC2001 HeadQuarters : L'Assembleur

L'ASSEMBLEUR Vu dans ... http://hxc2001.free.fr/coursasm

Pour bien comprendre l'assembleur il faut déjà comprendre le fonctionnement interne d'un ordinateur. En effet l'assembleur permet de toucher directement le système à tout les niveaux: Processeur, mémoires, périphéries,etc. Donc pour bien gérer tous ceci nous allons donc rappeler les grandes lignes du fonctionnement d`un système informatique....

L'ordinateur:

Un ordinateur se compose principalement d'un processeur (CPU), de mémoire (RAM et ROM), et de composants périphériques (Clavier, port série, contrôleur en tout genre, carte son....). Le CPU est bien sur le maître à bord dans un ordinateur (comme dans une console de jeux d'ailleurs), la mémoire et les périphériques étant à son service. Le CPU est connecté aux périphériques et à la mémoire à l'aide d'un BUS. Le BUS est une 'chaîne' ou tout les périphériques sont connectés en parallèle. Un BUS système se divise généralement en trois 'sous BUS' :

- Le BUS de données (c'est la partie qui transporte les données brutes). Notons que plus ce bus est large plus le système sera rapide. En effet un système 16 Bits est plus rapide qu'un système 8 bits car il est capable de 'transporter' 2 octets à la fois alors que le système 8 bits n'en transporte qu'un à la fois (1 octet=8bits)

- Le BUS d'adressage (c'est celui qui désigne un endroit dans la mémoire ou un périphérique). Plus il est large plus on peut adresser de la mémoire. Exemple : bus d' adressage 16bits =64Ko addressable ,bus 32bits=4Go!)

- Le BUS de contrôle (il désigne le mode de l'opération à effectuer: Lecture ou écriture, périphérique ou Mémoire ,Interruption,etc..)

Voici un exemple de diagramme d'un système:

Pour exécuter un programme le CPU va chercher les instructions dans la mémoire et les exécuter de façon séquentiel. Le CPU génère pour cela les signaux nécessaires sur le bus pour lire dans la mémoire. Chaque périphérique répond à une adresse précise (par exemple sur PC le clavier se situe a l'adresse n°96), et il ne doit pas y avoir 2 périphériques partageant la même adresse sinon il y a conflit ....(courant sur PC).
Notons qu'il y a des composants permettant d'accélérer le système: par exemple le DMA (Direct Memory Access) permet de faire des transferts entre un périphérique et la mémoire sans passer par le CPU. Les transferts sont alors très rapides et libère le CPU pour une autre tache.
Pour programmer sur un système il faut alors programmer le composant principal en premier lieu : Le CPU Pour cela on fait appel à l'Assembleur....

L'assembleur:

L'Assembleur permet de contrôler directement le CPU: Ceci permet d'avoir une totale maîtrise du système et surtout permet de faire des programmes très très rapide par rapport aux langages de haut niveau (C++ ,Basic,...). En effet, bien que ces langages permettent de faire des programmes facilement et rapidement, ils n'optimisent pas le code d'exécution et donc font des programmes (beaucoup) plus lent et beaucoup plus volumineux... Notons que l'on peut insérer de l'assembleur dans certains langages (Pascal et C par exemple) pour accélérer l'exécution du programme. L'assembleur permet donc de programmer le CPU, mais avant d'entrer dans le vif du sujet présentons le CPU: Le CPU (Central Processing Unit), charge, analyse et exécute les instructions présentes en mémoire de façon séquentiel,c'est à dire une instruction à la suite de l'autre. Le CPU contient une unité de calcul en nombre entier (ALU), d'un bus interne, d'un bus externe se connectant au système, d'un décodeur d'instructions qui comme son nom l'indique décode l'instruction compilée pour pouvoir l'exécuter, et des cases mémoires que l'on appelle les Registres.

Voici par exemple le diagramme interne du Z80:

Ce sont les registres qui permettent la 'communication' entre le programme et le CPU. Ils sont 'l'interface' du CPU. Pratiquement toutes les données qui passent par le CPU pour être traité par celui-ci, devront se trouver dans les registres du CPU. Ces cases mémoire sont les plus rapides de tout le système... Il y a différents types de registres dans un CPU: Les registres de traitements et les registres d'adressages,et les registres d’état.

Les Registres de traitement sont les registres destinés au traitement des valeurs contenues dans celle-ci. Par exemple on peut effectuer une addition d'un registre de traitement avec un autre registre, ou effectuer des multiplications, ou des traitements logiques......

Les Registres d'adressage permettent de pointer un endroit de la mémoire. Ils sont utilisés pour lire ou écrire dans la mémoire...

Les registres d’état (ou volet: FLAG en anglais) ,sont de petits registres (de 1 Bit) indiquant l'état du processeur et 'le résultat' de la dernière instruction exécutée. Les plus courants sont le Zéro Flag (ZF) qui indique que le résultat de la dernière opération est égale a zéro (après une soustraction par exemple) ,le Carry Flag (CF) qui indique qu'il y a une retenue sur la dernière opération effectuée,Overflow Flag qui indique un dépassement de capacité de registre....

Pour gérer ces registres on fait appel aux instructions du processeur : Ces instructions permettent d'effectuer des taches très simple sur les registres. Elles permettent de mettre des valeurs dans les registres, d'effectuer des traitements logiques (Fonctions OU ,ET ,...), des traitements arithmétiques, des traitements de chaîne de caractères , etc.
Ces instructions sont formées à l'aide de code binaire. Pour nous ils est beaucoup plus facile d'utiliser des mots clés à la place de ces codes binaire obscures... C'est pour cela que l'on fait appel aux compilateurs qui permettent de transformer un programme assembleur fait avec des mots clés compréhensibles pour nous (mais incompréhensibles pour la machine), en un programme exécutable, compréhensible par le processeur.
Ces mots clé ou mnémoniques, sont souvent la compression d'un mots ou d'une expression en anglais présentant l'action de l'instruction. Par exemple sur les processeurs 8086 l'instruction MUL permet la multiplication (MULtiply), sur Z80 l'instruction LD permet de charger une valeur dans un registre ou dans la mémoire (LoaD). Les instructions sont souvent suivit d'opérandes permettant d'indiquer sur quel(s) registre(s) on veut effectuer le traitement, quelle valeur on veut utiliser, etc.

Exemple : pour additionner 2 registres (2 registres 16 bits AX et BX) sur 8086 :
ADD AX,BX
Cette instruction correspond en gros à AX=AX+BX

La même chose sur 68000:
ADD.W D1,D0
Cette instruction correspond en gros à D0=D0+D1

Sur cet exemple nous pouvons remarquer la différence de syntaxe entre 2 processeurs différents: Lorsqu'un constructeur conçoit un processeur il détermine aussi son assembleur. C'est pour cela que l'assembleur n'est pas universel car il est différent sur chaque processeurs (ou plutôt concepteur). Et même si par hasard une instruction est identique entre 2 CPU différents, elle ne sera pas compatible sur chaque processeur car le code binaire correspondant sera différent. Ceci est l'un des plus gros inconvénient de la programmation en assembleur car il faut alors reprogrammer de A à Z le logiciel si on veut le porter sur un autre processeur.
Chaque processeur comporte un nombre plus ou moins important d'instructions. Ce nombre permet de séparer 2 grandes familles:
-Les CPU Risc comportant peu d'instructions mais étant plus rapides (instructions de même taille).
-Les CPU Cisc comportant plus d'instructions mais généralement moins performant.

Connaissant les registres et les instructions d'un processeur on peut alors commencer à le programmer.

Pour les details (registres, instructions,...) sur un processeur cliquez sur un des liens suivant:

Famille X86 (Essentielement PC)
Processeur 68000 (ATARI ST, AMIGA ,TI89 , etc....)
Processeur Z80 (ZX81 ,TI82, etc....)

Nous allons pas détailler ici chaque processeur car il faudrait beaucoup plus qu'un site Web.....
Allez voir les liens de ce site, il existe beaucoup de site parlant de la programmation en assembleur sur différents processeurs allant du Z80 au Pentium III en passant par le 68000 et autres...


Introduction au langage assembleur

But de ce sujet

L'objectif de ces pages est de donner un aperçu succinct du langage assembleur. Ce langage est dit de "bas niveau " car il est étroitement lié à l'architecture du microprocesseur. "Registres" , "Adresses mémoires", "Interruptions", "Appels système" ... voilà une série de notions qui risquent fort de rester théoriques. Ce serait dommage, alors qu'il est si simple de les aborder par la pratique à l'aide du langage assembleur et en visualisant à l'aide d'un debugger les instructions qui s'exécutent et les registres où cela se passe.

Les registres du processeur

Les registres décrits ci-dessous sont les registres du 8086, l'ancêtre des processeurs qui sont au c?ur de nos PC actuels. Ces processeurs ont évolué depuis tout en restant compatibles avec leur ancêtre (compatibilité ascendante). Cette description devrait donc suffire se faire une idée de la structure d'un microprocesseur.

Registres de données

AX, BX, CX et DX
Ce sont des registres 16 bits du 8086, ils peuvent chacun être scindés pour y entreposer deux variables d'un octet
Ainsi, le registre 16 bits AX peut être considéré comme l'adjonction de deux registres 8 bits AH et AL (H =high, L =low)

Certaines instructions dédient ces registres à des rôles spécifiques :

AX | "accumulator" l'accumulateur est privilégié pour certaines opérations arithmétiques ainsi que les opérations d'entrée/sortie
BX | "base register" il est appelé registre de base car l'adressage en mémoire peut se faire par son intermédiaire
CX | "count register" est implicitement le registre compteurs de boucles pour les instructions répétitives. (CL pour les opérations de décalage)
DX | "data register" sert dans certaines circonstances d'extension à l'accumulateur.

Registres d'adresses

Registres dits d'index : | SI | "Source index"
 | DI | "Destination index"
Registres de base | BP | "Base pointer"
 | SP | "Stack pointer"

Registres de segments

CS | "Code Segment" | = adresse de base pour le programme
DS | "Data Segment" | = adresse de base pour les données
ES | "Extra Segment" | = adresse de base pour d'autres données
SS | "Stack Segment" | = adresse de base pour la pile

Registre d'états

Ce registre contient les flags, ce sont des bits qui basculent d'un état à l'autre en fonction des résultats de l'exécution d'opérations arithmétiques ou logiques.
- Le flag Carry passe à 1 si une addition donne lieu à un report.
- Le flag Signe passe à 1 si le bit le plus significatif du résultat vaut 1.
- Le flag Zero passe à 1 si le résultat de la dernière opération est nul.
- etc.
Ces flags servent entre autre aux instructions de sauts conditionnels.

L'instruction pointeur

IP Aussi appelé compteur ordinal est un registre qui s'incrémente sans cesse. Il contient en permanence l'adresse de la prochaine instruction à exécuter. "Faire un saut" dans un programme revient à inscrire dans le registre IP l'adresse de l'instruction où le programme doit se rendre. L'incrémentation de l'Instruction Pointer reprend alors depuis cette nouvelle valeur pour poursuivre la nouvelle séquence d'instructions.

Les adresses mémoires

Les Pentium et AMD actuels utilisent deux modes de fonctionnement : le mode réel et le mode protégé.
Le mode protégé est géré par Windows ou Unix, le mode réel (on devrait dire mode segmenté) est l'état dans lequel on se trouve au démarrage de la machine. La capacité d'adressage est limitée à 1 Mo comme pour les premiers 8086.
Les adresses sont données sous la forme Segment : Offset

Exemple :

Sous DOS ( mais Windows émule parfaitement la chose) l'adresse B800:0000 est l'adresse de base de la mémoire vidéo en mode texte. Cette adresse est donc celle du byte contenant le code ASCII du caractère situé dans le coin supérieur gauche de l'écran.
Le segment B800 débute à l'adresse physique B8000.
L'adresse physique s'obtient en calculant Segment x 16 + Offset

Manipulation :

Avec l'utilitaire DEBUG, écrivons 41H = le code ASCII du A majuscule à l'adresse B800:0400 = adresse physique B8400 de sorte à faire apparaître ce 'A' au milieu de l'écran.

        C:\>debug
        -e B800:400
        B800:0400 20.41 07.4E 20.
        -q

Utilisation des registres pour former des adresses

Les adresses 20 bits sont constituées dans les registres en associant les registres de segment et les registres d'adresses. Les registres d'adresses sont associés par défaut à chacun des registres de segment.
CS est toujours utilisé avec IP pour former l'adresse logique CS:IP
SS est associé au Stack Pointer SP pour former l'adresse du dessus de la pile SS:SP
le Base Pointer BP est lui aussi par défaut associé au Stack Segment SS
DS le Data Segment sert en principe de segment de base pour les adresses formées avec les registres SI, DI et BX (source index, destination index et base register)

Ecrire en assembleur avec l'utilitaire DEBUG

Toujours avec le programme debug, la commande 'a' permet d'encoder des instructions saisies en assembleur.

        a
        mov ax,B800
        mov ds,ax
        mov cx,4E41
        mov di,400
        mov [di],cx
        ret

Utilisez la commande T pour tracer une à une, les instructions de ce bout de programme.

Elles placent l'adresse du segment B800 dans le registre DS (data segment)
Le registre CL reçoit le code ASCII d'un 'A' majuscule (41) (Ne soyez pas troublés par la valeur 4E inscrite dans le registre CH, elle va joindre des attributs au code ASCII contenu dans CL pour faire ressortir le caractère en jaune sur fond rouge)
Le nombre placé dans le registre DI indique le déplacement par rapport au début de la mémoire écran.

Les modes d'adressage

Nous n'avons encore utilisé qu'une instruction, l'instruction MOV.
L'exemple ci-dessus illustre déjà le fait que les opérandes peuvent être spécifiées de différentes manières. Ce sont les modes d'adressage.

MOV DS , AX | Adressage de registres la valeur du registre AX est recopiées dans le registre DS
MOV CX , 4E41 | Adressage immédiat la valeur immédiate 4E41 est recopiée dans CX
MOV [DI] , CX | Adresse en mémoire [DI] mis entre crochets signifie "à l'adresse donnée par DI" (cette adresse est l'offset à ajouter au segment de données)

L'instruction MOV attend deux opérandes, le premier indique la destination du déplacement de la donnée, le second opérande indique la source.
La destination peut être un registre, une adresse mémoire ou un registre de segment (sauf pour le registre CS qui ne peut jamais être une destination)
La source peut être un registre, une adresse mémoire, un registre de segment ou une valeur immédiate.

Le tableau ci-dessous inventorie toutes les manières d'utiliser l'instruction MOV

MOV r1 , r2 | r1 reçoit la valeur identique à celle contenue dans r2
MOV r , i | Le registre r est initialisé avec une valeur immédiate
MOV m , i | Ecriture d'une valeur immédiate en mémoire
MOV r , m | Lecture en mémoire à l'adresse m en destination du registre r
MOV m , r | Ecriture en mémoire à partir du registre r
Echanges entre registres de segment et la mémoire
MOV s , m | Lecture: s := m
MOV m , s | Ecriture: m := s
Echanges entre registres généraux et registres de segment
MOV s , r
MOV r , s

Dans cette liste les adresses 'm' sont spécifiées en plaçant une valeur de l'offset et/ou des noms de registres entre crochets. Exemples : [2000] ; [BP] ; [BP+2000] ; [SI] ; [BP+SI] ; [BP+SI+2000]

En fait toutes les combinaisons ne sont pas acceptées ! Les combinaisons valides sont celles que l'on forme en ne prenant pas plus d'un élément dans l'une des trois colonnes du tableau :

BX | SI | nombre
BP | DI

Autrement dit, il ne peut pas il y avoir simultanément dans une adresse 2 registres de base (dont le nom commence par b) ni 2 registres d'index (dont nom se termine par i )

Il est parfois nécessaire de préciser la taille de la valeur à lire. On fait alors précéder l'adresse de la donnée par BYTE PTR ou par WORD PTR selon que l'adresse désigne un ou deux octets.

Aperçu rapide de quelques autres instructions

Les instructions MOV nous ont été utiles pour illustrer les modes d'adressage mais avec les MOV il nous est juste possible de déplacer les données d'un endroit à l'autre. Il nous faut d'autres instructions pour faire des opérations arithmétiques et logiques, des sauts, des appels à des fonctions, à des interruptions etc.

Voici donc d'autres instructions. La liste est loin d'être complète mais elle doit suffire pour se faire une idée de ce que sont les instructions en général.

Opérations arithmétiques et logiques élémentaires : ADD SUB CMP AND TEST OR XOR
Modes d'adressage acceptés: r,m m,r r,r m,i et r,i

Multiplications et divisions: MUL IMUL DIV IDIV

Exemples : | MUL BYTE PTR Valeur | AX := AL x Valeur
 | MUL WORD PTR Valeur | DX.AX := AX x Valeur
 | DIV BYTE PTR Valeur | AL := AX / Valeur
 | DIV WORD PTR Valeur | AX := DX.AX / Valeur

Incrémentation, décrémentation, inversion logique, négation : INC DEC NOT NEG

Appel d'un sous-programme : CALL label

Sauts inconditionnels JMP label

Sauts conditionnels JZ (=JE) JNZ ( = JNE) JC JNC JS JNS

Assemblage et édition de liens

TASM est l'assembleur de Borland. Nous l'utilisons avec la commande TLink déjà utilisée avec l'environnement de développement intégré TC ou le compilateur TCC. La vérification de l'exécution se fait avec le Turbo Debugger " TD ". Tapez simplement TASM pour voir les options disponibles. Ces options sont assez rébarbatives aussi une fois que l'on sait celles qui nous conviennent on a tout intérêt à les fixer une fois pour toute dans un fichier de commande.

Voici les fichiers A.bat pour commander l'assemblage et L.bat pour demander l'édition de liens.

A.bat : TASM /zi /c /la %1.asm;

L.bat : TLINK /v /k %1.obj;

Pour rappel, on part d'un code source ".asm" L'assemblage en fait un code objet ".obj" relogeable qui à son tour est traité par l'éditeur de lien pour en faire un code exécutable ".exe" Accessoirement l'assembleur produit aussi un "listing" qui est un document destiné à être imprimé. On y retrouve côte à côte, le code source et le code binaire correspondant noté en hexadécimal.

Du source à l'exécutable

... à suivre


La bible du cracker par Falcon

Bible du cracker
Cours d'assembleur par Falcon

SOMMAIRE

1 - Les bases indispensables pour débuter

    A - Définition de l'assembleur
    B - Le language hexadécimal
    C – Le calcul binaire
    D – Conversion binaire ó hexadécimal
    E - Le processeur et ses registres

      a ) Les registres généraux.
      b ) Les registres pointeurs ou d'offset
      c ) Le processeur et ses registres de segment
      d ) Le registre Flag

2 - Les premières instructions

    A - La première instruction : MOV
    B - Une autre instruction : JMP
    C - Quelques instructions arithmétiques : ADD et SUB

3 - Pile - Interruptions

    A - La pile (Stack) et ses instructions

      a ) PUSH.
      b ) POP.

    B - Les interruptions - Instructions

4 - Les flags - Les sauts conditionnels - CMP

    A - Les flags - Les indicateurs

      a ) CF
      b ) PF
      c ) AF
      d ) ZF
      e ) SF
      f ) IF
      g ) DF
      h ) OF

    B - Les instructions conditionnelles

      JB - JNAE - JC
      JAE - JNB - JNC
      JE - JZ
      JNE - JNZ
      JO - JNO
      JP - JPE
      JNP - JPO
      JS - JNS
      JA - JNBE
      JBE - JNA
      JG - JNLE
      JGE - JNL
      JL - JNGE
      JLE - JNG

    C - L'instruction CMP

5 - Instructions mathématiques

    A - Les instructions mathématiques

      a ) MULTIPLICATION : MUL / IMUL
      b ) DIVISION : DIV / IDIV
      c ) SHR et SHL
      d ) NEG

    B - Les nombres à virgules
    C - Les nombres négatifs
    D - Les instructions logiques

      a ) AND
      b ) OR
      c ) XOR
      d ) NOT
      e ) TEST

6 - La mémoire et ses instructions

7 - Les instructions assembleur

8 - Table ASCII

L’ASSEMBLEUR

1 - Les bases indispensables pour débuter

Pour cracker n’importe quel logiciel, il est indispensable de connaître le fonctionnement de l’assembleur et ses instructions.
Pour cela, je vous conseille vivement d’acheter les 2 livres suivants :
Assembleur " Une découverte pas à pas " de Philippe Mercier - Edition Marabout n°885 (environ 50 francs).
Assembleur " Théorie, pratique et exercices " de Bernard Fabrot - Edition Marabout n°1087 (environ 50 francs).

Comme vous le verrez, ce cours est surtout destiné à la programmation en asm.

Retour au sommaire

A - Définition de l'assembleur

L'assembleur est un language de programmation transformant un fichier texte contenant des instructions, en un programme que le processeur peut comprendre (programme en language machine).

Ce language machine a la particularité d'être difficile à programmer car il n'est composé que de nombres en hexadécimal (base 16). L'assembleur est une "surcouche" du language machine, il permet d'utiliser des instructions qui seront transformées en language machine donc il présente une facilité de programmation bien plus grande que le language machine. Le fichier texte qui contient ces instructions s'appelle le source.

Retour au sommaire

B - Le language hexadécimal

Nous allons aborder un aspect très important de la programmation en assembleur : le système de numérotation en hexadécimal.
Ce système est basé sur l'utilisation des chiffres et de certaines lettres de l'alphabet (de A à F). Vous connaissez bien entendu le système décimal (base 10).
En assembleur, les nombres décimals sont suivis d'un "d" (1000=1000d) mais en principe la majorité des assembleurs calculent en décimal par défaut.
La notation hexadécimale (base 16) implique qu’il faut disposer de 16 signes alignables dans une représentation et, comme les chiffres ne suffisent plus, on a décidé que les signes de 0 à 9 seraient représentés pas les chiffres 0..9 et les signes manquants pour obtenir 16 signes seraient les 6 premières lettres de l’alphabet soit A, B, C, D, E, F avec :

Hexadécimal | Décimal
A | 10
B | 11
C | 12
D | 13
E | 14
F | 15

Nous ne pouvons pas utiliser le G et les lettres qui suivent, donc nous augmenterons le premier chiffre ce qui donne 16d=10h. Continuez ainsi jusqu'à 255 qui fait FF. Et après. Et bien oncontinue. 256d=0100h (le h et le zéro qui précède indiquent que ce nombre est en hexadécimal). 257d=101h. 65535=FFFFh. Bon, je pense que vous avez compris.

Pour convertir des nombres du décimal en hexadécimal, vous pouvez utiliser la calculatrice de Windows en mode scientifique.

Exemples :
8 = 8*1
78 = 7*16 + 8*1
A78 = 10*256 + 7*16 + 8*1
EA78 = 14*4096 + 10*256 + 7*16 + 8*1

Retour au sommaire

C - Le calcul binaire

Je vais finir mon explication avec le calcul binaire. Vous savez probablement que les ordinateurs calculent en base 2 (0 ou 1). Le calcul binaire consiste à mettre un 1 ou un 0 selon la valeur désirée. Chaque 0 ou 1 est appelé un bit, 8 bits forment un octet.

Pour le nombre 1, nous avons 00000001b (le b signifie binaire). Un nombre binaire se décompose comme-ceci :
128|64|32|16|8|4|2|1
Pour le nombre 1, la première case est remplie (1=rempli). Pour avoir le nombre 2, nous aurons 00000010b et pour le nombre 3, nous aurons 00000011 (1 case=1 - 2ème case=1 => 1+2=3).
Le nombre 11111111b donnera 255 puisque en additionnant 1+2+4+...+128, on obtient 255. Plus il y a de bits, plus le nombre peut être grand, nous verrons plus tard qu'il existe différentes unités selon le nombre de bits.
Exemples :

Base 2 | Base 10
00000001 | 1 = 1*1
00000011 | 3 = 1*2 + 1*1
00001011 | 11 = 1*8 + 0*4 * 1*2 + 1*1
00011011 | 27 = 1*16 + 1*8 + 0*4 + 1*2 + 1*1

Retour au sommaire

D - Conversion binaire ó hexadécimal

Voici un exemple démontrant la simplicité de conversion d’un nombre binaire en hexadécimal :

Soit le nombre hexadécimal EA78 à convertir en binaire, 16 étant 2 à la puissance 4, chaque signe de la représentation hexadécimale sera converti en 4 signes binaires et le tour sera joué :

Hexadécimal | Décimal | Binaire
E | 14 | 1110
A | 10 | 1010
7 | 7 | 0111
8 | 8 | 1000

En alignant les représentations binaires obtenues, on trouve le nombre binaire suivant : 1110101001111000.
Inversement, pour convertir un nombre binaire en hexadécimal, c’est tout aussi simple, on regroupera les bits par groupe de 4 :
Soit 1110101001111000 à traduire en hexadécimal :
On le découpe d’abord en 4 : 1110 1010 0111 1000

Binaire | Décimal | Hexadécimal
1110 | 14 | E
1010 | 10 | 10
0111 | 7 | 7
1000 | 8 | 8

Retour au sommaire

E - Le processeur et ses registres

Le processeur est composé de différentes parties. Les registres sont les éléments les plus importants du processeur pour celui qui programme en asm. Les registres sont souvent représentés comme des cases dans lesquelles sont stockées différentes valeurs de différentes tailles selon le type et le nom du registre. Il existe plusieurs types de registres :

  • registres généraux ou de travail

ils servent à manipuler des données, à transférer des paramètres lors de l’appel de fonction DOS et à stocker des résultats intermédiaires.

  • registres d’offset ou pointeur

ils contiennent une valeur représentant un offset à combiner avec une adresse de segment

  • registres de segment

ils sont utilisés pour stocker l’adresse de début d’un segment. Il peut s’agir de l’adresse du début des instructions du programme, du début des données ou du début de la pile.

  • registre de flag

il contient des bits qui ont chacun un rôle indicateur.

Retour au sommaire

a ) Les registres généraux.

On a plusieurs registres généraux (de travail) qui commencent par A,B,C et D. Ces quatre registres sont les plus utilisés.
Le 1er, AX (registre de 16 bits) qui se divise en deux petits registres de 8 bits, AL (l=low=bas) et AH (h=high=haut).
Il est utilisé lors d’opérations arithmétiques.
Nous avons ensuite BX (BL et BH), CX (CL et CH) et DX (DL et DH), ces registres sont divisés comme AX en 2 parties hautes et basses.
On peut rajouter un "E" devant les registres 16 bits afin d'obtenir un registre 32 bits.
Ce qui donne EAX,EBX,ECX et EDX. Notez que l'on ne peut avoir de EAH ou ECL. La partie haute d'un registre 32 bits, n'est pas directement accessible, on doit utiliser différentes instructions afin de la faire "descendre" dans un registre de 16 bits et pouvoir finalement l'utiliser.
Ces registres peuvent contenir une valeur correspondant à leur capacité.
AX=65535 au maximum (16 bits) et AL=255 au maximum (8 bits). Je répète que la partie haute du registre 32 bits ne peut pas être modifiée comme un registre. Elle peut être modifiée seulement si l'on modifie tout le registre 32 bits (y compris la partie basse), ou par le biais de quelques instructions qui permettent de copier la partie basse du registre dans la partie haute, mais cela ne nous concerne pas pour l'instant.
Les processeurs 286 et moins ne peuvent pas utiliser les registres 32 bits (EAX,EBX,ECX,EDX = impossible). Ce n'est qu'à partir du 386 que l'utilisation du 32 bits est possible.

Retour au sommaire

b ) Les registres pointeurs ou d'offset

Pour terminer, je vais parler des registres pointeurs qui sont DI (destination index), SI (source index), BP (base pointer), IP (instruction pointer) et SP (stack pointer).

Ces registres peuvent eux aussi être étendus à 32 bits en rajoutant un "E" (EDI,ESI,EIP,ESP,EBP).

Ces registres n'ont pas de partie 8 bits, il n'y a pas de EDL ou de ESH.

  • SI est appelé " Source Index ". Ce registre de 16 bits est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est associé au registre de segment DS.
  • DI est appelé " destination Index ". Ce registre de 16 bits est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est normalement associé au registre de segment DS ; dans le cas de manipulation de chaînes de caractères, il sera associé à ES.
  • BP ou " Base Pointer ". Ce registre de 16 bits est associé au registre de segment SS (SS :BP) pour accéder aux données de la pile lors d’appels de sous-programmes (CALL).
  • SP ou " Stack Pointer ". Ce registre de 16 bits est associé au registre de segment SS (SS :SP) pour indiquer le dernier élément de la pile.
  • IP est appelé " Instruction Pointer ". Ce registre de 16 bits est associé au registre de segment CS (CS :IP) pour indiquer la prochaine instruction à exécuter. Ce registre ne pourra jamais être modifié directement ; il sera modifié indirectement par les instructions de saut, par les sous-programmes et par les interruptions.

Les registres SI et DI sont le plus souvent employés pour les instructions de chaîne et pour accéder à des blocs en mémoire.

Le registre SP est utilisé pour accéder à la pile.

Nous ne l'employons presque pas, sauf pour du code très spécifique.

Ces registres (sauf IP) peuvent apparaître comme des opérandes dans toutes les opérations arithmétiques et logiques sur 16 bits.

Retour au sommaire

c ) Le processeur et ses registres de segment

Pour pouvoir chercher et placer des choses dans sa mémoire, un ordinateur a besoin de ce qu'on appelle une adresse. Celle-ci se compose de deux parties de 32 bits (ou 16 bits, cela dépend du mode employé). La première est le segment et la deuxième, l'offset.

Voici un exemple :

0A000h:00000h (adresse de la mémoire vidéo)

Dans cet exemple, il s'agit d'un adressage 16 bits. L'adressage 16 bits s'utilise en mode _réel_. Le mode réel est un état du processeur où il ne peut accéder à des blocs de taille supérieure à 16 bits (65536 bytes). Le 286 introduira un mode dit protégé qui permet de franchir cette barrière pour pouvoir accéder à quasiment tout la RAM en un bloc (ce mode protégé sera grandement amélioré à partir du 386).

Nous nous contenterons du mode réel au début, car il est plus simple mais il présente tout de même des limites. Sur 386, les segments ont été conservés pour garder une certaine comptabilité avec les vieilles générations de PC.

Les registres de segment ne sont que lus et écris sur 16 bits. Le 386 utilise un offset de 32 bits (sauf en mode réel où il travaille toujours en 16 bits). Voici la liste de ces segments, ils sont au nombre de 6 :

  • CS (code segment) = segment de code

Ce registre indique l’adresse du début des instructions d’un programme ou d’une sous-routine

  • SS (stack segment) = segment de pile

Il pointe sur une zone appelée la pile. Le fonctionnement de cette pile seront expliqués au chapitre 3.

  • DS (data segment) = Segment de données

Ce registre contient l’adresse du début des données de vos programmes. Si votre programme utilise plusieurs segments de données, cette valeur devra être modifié durant son exécution.

  • ES (extra segment) = Extra segment

Ce registre est utilisé, par défaut, par certaines instructions de copie de bloc. En dehors de ces instructions, le programmeur est libre de l’utiliser comme il l’entend.

  • FS (extra segment - seulement à partir du 386) = segment supplémentaire
  • GS (extra segment - seulement à partir du 386) = segment supplémentaire

Ces deux derniers registres ont un rôle fort similaire à celui du segment ES

Retour au sommaire

d ) Le registre Flag

Le registre FLAG est en ensemble de 16 bits. La valeur représentée par ce nombre de 16 bits n’a aucune signification en tant qu’ensemble : ce registre est manipulé bit par bit, certains d’entre eux infuenceront le comportement du programme. Les bits de ce registres sont appelés " indicateurs ", on peut les regrouper en deux catégories :

  • Les indicateurs d’état :
Bit | Signification | Abréviation
0 | « carry » ou retenue | CF
2 | parité | PF
4 | retenue auxiliaire | AF
6 | zéro | ZF
7 | signe | SF
11 | débordement (overflow) | OF
  • Les indicateurs de contrôle :
Bit | Signification | Abréviation
8 | trap | TF
9 | interruption | IF
10 | direction | DF

Les bits 1, 5, 12, 13, 14, 15 de ce registre FLAG de 16 bits ne sont pas utilisés.

Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs. Les instructions conditionnelles testent la valeur des indicateurs et agissent en fonction du résultat. La description détaillée des flags est faite au chapitre 4.

Retour au sommaire

2 - Les premières instructions

A - La première instruction : MOV

Cette instruction vient de l'anglais "move" qui signifie DEPLACER mais attention, le sens de ce terme est modifié car l'instruction MOV ne déplace pas mais place tout simplement.

Cette instruction nécessite deux opérandes (deux variables) qui sont la destination et la source. Ceux-ci peuvent être des registres généraux ou des emplacements mémoire. Cependant, les deux opérandes ne peuvent pas être toutes les deux des emplacements mémoire. De même, la destination ne peut pas être ce qu'on appelle une valeur immédiate (les nombres sont des valeurs immédiates, des valeurs dont on connaît immédiatement le résultat) donc pas de MOV 10,AX. Ceci n'a pas de sens, comment pouvez-vous mettre dans le nombre 10, la valeur de AX ? 10 n'est pas un registre.

Une autre règle à respecter, les opérandes doivent être de la même taille donc pas de MOV AX,AL, cela me semble assez logique. On ne peut pas utiliser une valeur immédiate avec un registre de segment (MOV ES,5 = impossible mais MOV ES,AX = possible).

Le registre DS est le segment de données par défaut.

Ainsi au lieu de :

MOV AX, DS :[BX]

MOV AX, DS :[SI+2]

…

On aura :

MOV AX, [BX]

MOV AX, [SI+2]

…

Quelques exemples :

MOV AL,CL (place le contenu de CL dans AL) donc si AL=5 et CL=10, nous aurons AL=10 et CL=10.

MOV CX,ES:[DI] (place dans CX, le contenu 16 bits de l'emplacement ES:[DI]). Donc si le word (le word est l'unité correspondant à 16 bits) ES:[DI]=34000, nous aurons CX=34000.

MOV CL,DS:[SI] (place dans CL, le contenu 8 bits (byte) de l'emplacement DS:[SI]).

Donc si DS:[SI] (attention en 8 bits) = 12, CL vaudra 12.

Au sujet de ces emplacements mémoires, il faut que vous vous les représentiez comme des "cases" de 8 bits, comme le plus petit registre fait 8 bits, on ne peut pas diviser la mémoire en emplacements plus petits. On peut prendre directement 16 bits en mémoire (un word) ou carrément un dword (32 bits). Il est très important de saisir cette notion. Un exemple concret : vous avez des livres sur une étagère, le livre se trouvant toute à gauche est le 1er de la liste, nous nous déplaçons de gauche à droite.

Un livre est un byte (8 bits), 2 livres sont un word (16 bits), 4 livres font un dword (32 bits). Si nous faisons MOV AX,ES:[DI], nous allons prendre 2 livres, en commençant par le livre se trouvant à l'emplacement DI et en prenant ensuite le livre se trouvant à l'emplacement DI+1 (l'emplacement suivant dans votre rangée de livre, 1 byte plus loin en mémoire).

Voici un autre exemple :

MOV EAX,ES:[DI] (place un dword depuis ES:[DI] dans EAX). C'est comme si on copiait 4 livres dans EAX.

Et quelques exemples incorrects :

MOV 1,10 (impossible et pas logique)

MOV ES:[DI],DS:[SI] (incodable malheureusement)

MOV ES,10 (incodable, il faut passer par un registre général donc MOV ES,AX ou MOV ES,CX mais pas de MOV ES,AL).

Ce qu'il faut retenir c'est que l'instruction MOV est comme le signe '=' MOV ES,CX c'est comme faire ES=CX.

Retour au sommaire

B - Une autre instruction : JMP

JMP est une simplification pour exprimer le mot JUMP qui signifie SAUTER en anglais.

JMP va servir dans le programme pour "passer" d'une opération à une autre, de sauter dans le programme à différents endroits pour effectuer d'autres tâches.

Je ne vais pas m'attarder dans des explications sur le pointeur d'instruction mais sachez que chaque instruction est désignée par un emplacement en mémoire (défini par CS:[IP]).

L'instruction JMP va donc modifier la valeur de IP et par conséquence changer d'instruction.

On peut utiliser JMP avec des valeurs immédiates.

Exemples :

JMP 10 (il aura pour effet de passer, non pas à l'instruction qui se trouve à la ligne 10 , mais à l'emplacement mémoire 10. Une instruction n'est pas forcément de taille 1, certaines peuvent s'étaler sur plusieurs bytes.)

Il est plus simple d'utiliser des "étiquettes qu'on peut écrire sous la forme "ETIQUETTES:".

Avec les étiquettes, il suffit d'écrire JMP ETIQUETTES pour arriver directement à l'instruction qui suit le étiquette. Un petit exemple de code:

DEBUT :

MOV AX,14

JMP DEBUT

Ce programme ne fait rien sinon placer continuellement 14 dans AX, on appelle cela une boucle.

Nous verrons plus tard qu'il existe différents types de sauts.

Retour au sommaire

C - Quelques instructions arithmétiques : ADD et SUB

ADD sert à additionner, et nécessite deux opérandes : une source et une destination.

La destination prendra la valeur de source + destination. Les règles de combinaison sont les mêmes que pour MOV mais avec SUB et ADD, l'utilisation des registres de segment est impossible.

Exemples :

ADD AX,CX (si AX=10 et CX=20 alors nous aurons AX=30 et CX=20)
ADD EAX,EBX
ADD AX,-123 (l'assembleur autorise les nombres négatifs)
SUB DI,12

Quelques exemples impossibles :

ADD AX,CL (comme pour MOV, un 16 bits avec un 8 bits sont incompatibles)
SUB ES:[DI],DS:[SI]
ADD ES,13 (impossible car c'est un registre de segment)

Retour au sommaire

3 - Pile - Interruptions

A - La pile (Stack) et ses instructions

La pile (stack en anglais) est un emplacement où des données de petites tailles peuvent être placées. Cette mémoire temporaire (elle se comporte comme la mémoire vive mais en plus petit) est utilisée aussi dans les calculatrices et dans tous les ordinateurs. Le système qu'elle emploie pour stocker les données est du principe du "dernier stocké, premier à sortir" (LIFO -last in, first out). Cela veut dire que lorsqu'on place une valeur (un registre, un nombre) dans la pile, elle est placée au premier rang (placée en priorité par rapport aux autres valeurs dans la pile). Lors de la lecture de la pile pour récupérer la valeur, ce sera la première qui sera prise. La pile se comporte comme une pile d'assiettes à laver.

Au fur et à mesure que les assiettes sont sales, vous les empilez. L'assiette qui se trouve tout en bas sera la dernière à être lavée, tandis que la première assiette empilée sera lavée au tout début. Même chose en asm, sauf les assiettes vont finir dans des registres.

Avant que n'importe quel appel de fonction (API Windows par exemple), le programme doit pousser tous les paramêtres auxquels la fonction s'attend sur la pile. On utilise donc cette pile comme une mémoire d'échange des paramêtres entre fonction/programme, ou encore comme mémoire des variables locales des programmes/fonctions.

Prenons l'exemple suivant :

La fonction GetDlgItemText de l'API Windows exige les paramêtres suivants :

1 - handle of dialog box : l'handle de la boîte de dialogue.
2 - Identifier of control : identificateur du contrôle
3 - Address of buffer for text : adresse du tampon pour le texte
4 - Maximum size of string : format maximum pour la chaîne

Par conséquent ceux-ci ont pu être passés comme ainsi :

    MOV EDI, [ESP+00000220] --------; place l'handle de la boite de dialogue dans EDI
    PUSH 00000100 --------------------; push (4) taille maxi de la chaîne
    PUSH 00406130 --------------------; push (3) adresse du buffer pour le texte
    PUSH 00000405 --------------------; push (2) identificateur du contrôle
    PUSH EDI --------------------------; push (1) hanlde de la boite de dialogue
    CALL GetDlgItemText --------------; call la fonction

Facile hein ?? Ceci peut être une des manière les plus simples pour cracker un numéro de série, si vous savez l'adresse du tampon pour le numéro de série, dans ce cas-ci 00406130, poser juste un breakpoint (permettra d'arrêter le programme à cette adresse précise), et vous finirez habituellement vers le haut ou autour du procédé qui produit la vraie publication du serial !

Retour au sommaire

Les instructions de la pile

a ) PUSH.

Nous avons tout d'abord l'instruction PUSH qui signifie POUSSER. Cette instruction permet de placer une valeur au sommet de la pile. PUSH doit être accompagné d'une valeur de 16 ou 32 bits (souvent un registre) ou d'une valeur immédiate.

Exemple 1:

PUSH AX
PUSH BX

En premier lieu, AX est placé en haut de la pile et ensuite BX est placé au sommet, AX est repoussé au deuxième rang.

Exemple 2:

PUSH 1230
PUSH AX

Nous plaçons dans la pile, la valeur 1230 (qui aurait pu être aussi un 32 bits) et ensuite AX, ce qui place 1230 au deuxième rang.

Retour au sommaire

b ) POP.

Passons maintenant à l'instruction de "récupération" qui se nomme POP (sortir-tirer).Cette instruction demande comme PUSH, une valeur de 16 ou 32 bits (seulement un registre).Elle prend la première valeur de la pile et la place dans le registre qui suit l'instruction.

Exemple 1:

Nous avions PUSH AX et PUSH BX et maintenant nous allons les sortir de la pile en utilisant

POP BX
POP AX

Nous retrouvons les valeurs initiales de AX et BX mais nous aurions eu aussi la possibilité de faire d'abord POP AX et POP BX, ce qui aurait placé dans AX, la valeur BX (1er dans la pile) et dans BX, la valeur AX (2ème).

Exemple 2:

La première partie était PUSH 1230 et PUSH AX. Nous allons placer dans CX, la valeur de AX (1er de pile) et dans DX, le nombre 1230 (2ème puisque "pilé" après)

POP CX
POP DX

Dans CX, nous avons la valeur de AX et dans DX, nous avons 1230.

La pile est très utile pour garder la valeur d'un registre que l'on va utiliser afin de le retrouver intact un peu plus loin. Souvent dans des routines, le programmeur doit utiliser tout les registres mais leur nombre est limité (utilisation des registres au maximum = gain de vitesse). Le programmeur va donc utiliser les fonctions de la pile pour pouvoir utiliser les registres et garder leur valeur actuelle pour une utilisation ultérieure. Malheureusement les instructions de la pile ralentissent le programme, la pile étant de toute façon plus lente que les registres qui sont au coeur du CPU.

Retour au sommaire

B - Les interruptions - Instructions

Dans un ordinateur, il y a plusieurs périphériques (imprimante, souris, modem,...). Vous connaissez certainement le BIOS qui se charge chaque fois que vous allumez votre ordinateur.

Il y a différents BIOS qui dépendent des constructeurs mais ils sont généralement tous compatibles aux niveaux des interruptions. Le BIOS possède différentes fonctions, ce sont les interruptions. Ces interruptions sont divisées en 7 parties principales (il y en a d'autres):

    - Fonctions vidéos (int 10h)
    - Fonctions disques (int 13h)
    - Fonctions de communication sur port série (int 14h)
    - Fonctions système - Quasiment inutiles (int 15h)
    - Fonctions clavier (int 16h)
    - Fonctions imprimante (int 17h)
    - Fonctions de gestion de l'horloge (int 1Ah)

Il n'y en a qu'une qui est importante, il s'agit de INT. Elle permet d'appeler une des 7 catégories d'interruptions. Pour pouvoir sélectionner une fonction, il faut configurer les registres avec certaines valeurs que vous trouverez dans la liste d'interruptions (une dernière fois, procurez-vous en une !!!). Souvent, il faut juste changer AX mais parfois, tout les registres sont modifiés. Ensuite on appelle INT accompagné du numéro de catégorie de la fonction désirée. Par exemple, pour rentrer en mode texte 80x25 caractères, il faut écrire ceci:

MOV AX,03h

INT 10h

Le nombre 03 qui se trouve dans AX, signifie que nous voulons le mode texte 80x25 (13h est le mode 320x200x256 couleurs). L'instruction INT 10h appelle la catégorie 10h (fonctions vidéos).

Et c'est tout. Les interruptions permettent de faire plus facilement certaines actions qui demanderaient une programmation plus compliquée.

Retour au sommaire

4 - Les flags - Les sauts conditionnels - CMP

A - Les flags - Les indicateurs

Les flags, "indicateurs", sont des bits qui donnent certaines informations. Ils sont regroupés dans le registre de flag. Ces flags sont modifiés en fonction de l'exécution des différentes instructions. Celles-ci changent la valeur des flags selon le résultat obtenu. Voici une liste des différents flags et leur utilité. Les bits marqués du 1 ou du 0 ne sont pas utilisés.

    Bit 1 : CF
    Bit 2 : 1
    Bit 3 : PF
    Bit 4 : 0
    Bit 5 : AF
    Bit 6 : 0
    Bit 7 : ZF
    Bit 8 : SF
    Bit 9 : TF
    Bit 10 : IF
    Bit 11 : DF
    Bit 12 : OF
    Bit 13 : IOPL
    Bit 14 : NT
    Bit 15 : 0
    Bit 16 : RF
    Bit 17 : VM

Nous n'étudierons que les 12 premiers, ce sont les plus importants.

a ) CF

Retenue

Nous avons tout d'abord CF (Carry Flag). C'est le flag dit de retenue.

Dans les opérations mathématiques, il arrive que le résultat de l'opération soit codé sur un nombre supérieur de bits. Le bit en trop est placé dans CF. De nombreuses instructions modifient aussi CF. Par exemple, les instructions CLC et CMC, la première mettant à zéro CF et la deuxième qui inverse la valeur de CF. STC (set carry) met le flag à 1.

b ) PF

Parité

Il s'agit du Parity Flag. La valeur de ce flag est 1 si le nombre de bits d'une opérande (paramètre d'une instruction) est pair.

c ) AF

Retenue auxiliaire

AF est l'auxiliary carry qui ressemble à CF.

d ) ZF

Zéro

Il s'agit du Zero Flag qui est misà un lorsque un résultat est égal à 0. Souvent utilisé pour les diverses opérations, il est utile pour éviter des problèmes de divisions (je vous rappelle que diviser par zéro est impossible).

e ) SF

Signe

SF signifie Signe Flag. Simplement, sa valeur passe à 1 si nous avons un résultat signé (négatif ou positif).

f ) IF

Interruption

IF pour Interrupt Flag, enlève la possibilité au processeur de contrôler les interruptions.

Si IF=0, le processeur ne commande pas et si IF=1 alors c'est le contraire.

L'instruction STI provoque IF=1 et CLI met IF=0.

g ) DF

Direction

Le flag DF est le Direction Flag. C'est ce Flag qui donne l'indication sur la manière de déplacer les pointeurs (références) lors des instructions de chaînes (soit positiviment, soit négativement).

Deux instructions lui sont associées, il s'agit de CLD et STD.

h ) OF

Débordement

OF est l'Overflow Flag (indicateur de dépassement). Il permet de trouver et de corriger certaines erreurs produites par des instructions mathématiques. Très utile pour éviter les plantages. Si OF=1 alors nous avons affaire à un Overflow. Il existe une instruction qui s'occupe de ce Flag, c'est INTO qui déclenche l'exécution du code qui se trouve à l'adresse 0000:0010 (interruption 4).

C'en est tout pour les flags, vous comprendrez leur utilité au fur et à mesure de votre progression mais passons aux instructions conditionnelles qui leur sont directement associées.

Retour au sommaire

B - Les instructions conditionnelles

Nous abordons une partie qui est nécessaire lors de la création d'un programme. Souvent, le programme doit faire une action selon la valeur d'un résultat. Les instructions conditionnelles comme leur nom l'indique, sont des instructions qui font une action selon un résultat. Elles se basent sur les flags pour faire leur choix.

Vous vous souvenez de l'instruction JMP, il s'agissait d'un simple saut vers une autre partie du programme. D'autres instructions comme JMP font des sauts mais selon certains critères, on les appelle des sauts conditionnels. Voici la liste des ces instructions avec la valeur de l'indicateur nécessaire à l'exécution.

    JB - JNAE - JC
    Below - Not Above or Equal - Carry
    CF = 1
    JAE - JNB - JNC
    Above or Equal - Not Below - Not Carry
    CF=0
    JE - JZ
    Equal - Zero
    ZF=1
    JNE - JNZ
    Not Equal - Not Zero
    ZF=0
    JO - JNO
    Overflow - Not Overflow
    OF=1 - OF=0
    JP - JPE
    Parity - Parity Even
    PF=1
    JNP - JPO
    No Parity - Parity Odd
    PF=0
    JS - JNS
    Signed - Not Signed
    SF=1 - SF=0
    JA - JNBE
    Above - Not Below or Equal
    CF=0 et ZF=0
    JBE - JNA
    Below or Equal - Not Above
    CF=1 ou ZF=1
    JG - JNLE
    Greater - Not Less or Equal
    ZF=0 et SF=OF
    JGE - JNL
    Greater or Equal - Not Less
    SF=OF
    JL - JNGE
    Less - Not Greater or Equal
    SF (signé)=OF
    JLE - JNG
    Less or Equal - Not Greater
    ZF=1 ou SF (signé)=OF

Pour tester tout cela, nous avons besoin d'une instruction, c'est CMP qui nous aidera à le faire.

Retour au sommaire

C - L'instruction CMP

Cette instruction va servir à tester différentes valeurs et modifier les flags en fonction du résultat. CMP est un SUB qui ne change ni la source ni la destination, seulement les flags. Un CMP BX,CX sera comme un SUB BX,CX à l'exception près que BX ne sera pas modifié.

Si BX=CX alors BX-CX=0 donc le flag ZF sera égal à 1. Si nous voulons faire un saut avec "égal à" (JNE ou JZ qui demande ZF=1), nous avons ZF=1 et comme JZ demande que ZF=1 pour faire le saut, nous avons donc le saut. Souvenez-vous simplement que la plupart du temps, il s'agit de comparaisons du genre :

effectue le saut :

plus grand que (nombres non-signés) ---> JA

plus petit que (nombres non-signés) ---> JB

plus grand que (nombres signés) -------> JG

plus petit que (nombres signés) -------> JL

égal à (signé et non-signé) -----------> JE ou parfois JZ

il suffit de rajouter un 'n' après le 'j' pour avoir la même instruction mais exprimée de facon négative

ne saute pas si :

plus grand que (nombres non-signés) ---> JNA (jump if _not above_)

...et ainsi de suite.

Retour au sommaire

5 - Instructions mathématiques

A - Les instructions mathématiques

a ) MULTIPLICATION : MUL / IMUL

Je vais continuer avec d'autres instructions pour compléter et vous permettre d'effectuer d'autres opérations. Pour l'instant, le cours est bien théorique mais attendez encore un peu...

Bon, commençons par la multiplication, très utile. L'assembleur utilise l'instruction MUL pour les opérations non signées et IMUL pour les opérations signées. MUL nécessite une opérande (un paramètre). Cette opérande s'appelle la source. Le nombre qui doit être multiplié se trouve OBLIGATOIREMENT dans AX. La source est un registre (BX,CX). Si vous avez AX=2 et BX=5 et que vous faîtes MUL BX, vous obtiendrez AX=10 et BX=5.

Pour IMUL, c'est pareil, les assembleurs acceptent de mettre des nombres négatifs dans un registre (MOV AX,-10). Pour ce qui est des nombres négatifs, sur 16 bits par exemple, -10 sera comme 65525 (65535-10). Le processeur soustrait à la valeur limite du registre (ici 16 bits= 65535), la valeur absolue du nombre négatif.

Donc pour 16 bits, les nombres signés iront de -32768 à 32767. IMUL s'utilise comme MUL sauf qu'il faut indiquer que l'on travaille avec des nombres signés ou non-signés. C'est pourquoi, il faut toujours effacer DX (mettre DX à 0) lors d'une multiplication non-signée.

Dans le cas d'un IMUL, il faut utiliser l'instruction CWD (convert word to doubleword), qui 'étend' le signe de AX dans DX. Omettre ces instructions peut conduire à des résultats erronés. De même, multiplier par DX donne de drôles de résultats.

Retour au sommaire

b ) DIVISION : DIV / IDIV

Comme pour MUL et IMUL, nous avons DIV (nombres non signés) et IDIV (valeurs signées). DIV divise la valeur de AX par le source. Comme pour IMUL et MUL, il faut que DX soit correctement paramétré.

Donc si nous avons AX=12 et BX=5, ensuite IDIV BX, nous aurons AX=2 et DX=2 (DX est le reste).

N'oublier pas qu'on ne peut pas diviser par 0 !!! Si vous passez outre, le programme retourne au DOS et affiche 'Div by 0'.

Retour au sommaire

c ) SHR et SHL

SHR permet de diviser plus rapidement les registres. SHR demande deux opérandes: le nombre à diviser et le diviseur. SHR divise par une puissance de deux.

Pour diviser par deux AX,BX,CX ou DX (et les autres registres aussi), on met SHR AX,1 (1 parce que 2^1=2), si on veut diviser BX par 4, on fait SHR BX,2 (2 car 2^2=4), SHR CX,3 (2^3=8) divise CX par 8. Compris ? C'est très rapide, pour diviser AX par 256, on aura SHR AX,8. SHL, c'est pareil sauf qu'on multiplie : SHL AX,4 (2^4=16) multiplie AX par 16. On peut combiner aussi des SHL pour multiplier par une valeur qui n'est pas une puissance de 2.

Si nous voulons multiplier AX par 320, on remarque que X*320 = X*256+X*64, or 64 et 256 sont des puissances de 2, donc nous allons multiplier par 256 et multiplier par 64, et additionner les 2 résultats :

MOV AX,12 ;on veut multiplier 12 (=X)

MOV BX,AX ;on copie dans BX (AX=X ; BX=X)

SHL AX,8 ;on multiplie par 256 (AX=X*256)

SHL BX,6 ;on multiplie par 64 (BX=X*64)

ADD AX,BX ;on additionne dans AX (AX=X*256 + BX) -> (AX=X*256+X*64=X*320)

et voila. cependant dans certains cas, il est préférable d'utiliser MUL car une décomposition en puissance de 2 prendrait plus de temps...

Retour au sommaire

d ) NEG

NEG demande une opérande : la destination. Il change le signe de la destination. (en inversant tous les bits et ensuite en ajoutant 1)

Par exemple si AX=12, alors NEG AX donnera AX=-12. Si CL=-3 alors NEG CL changera la valeur de CL en 3. Simple non ?

Retour au sommaire

B - Les nombres à virgules

Le problème en assembleur, est que nous ne pouvons utiliser les nombres à virgules directement dans les registres (sauf avec un coprocesseur mais il demande une programmation différente basée sur une pile). Nous utiliserons les nombres à virgule fixe. Il s'agit simplement d'effectuer des calculs avec des valeurs assez grandes qui seront ensuite, redivisée pour retrouver un résultat dans un certain intervalle.

Nous voudrions avoir 0.5 dans AX, mais AX n'accepte pas de nombre à virgule, nous allons simplement multiplier AX par 256, or 256*0.5=128, parfait, c'est un nombre entier :

MOV AX,128

maintenant, nous allons multiplier 300 par 0.5 (donc par 128)

MOV CX,300
XOR DX,DX ;comme MOV DX,0
MUL CX

dans AX, nous avons (300*128), mais ce nombre est 256x trop grand ! il suffit de le rediviser (qui est en fait un simple décalage peu coûteux en temps de calcul) :

SHR AX,8

Nous avons dans AX = (300*128)/256 = 150. Et voilà ! nous avons effectué une MUL avec un nombre à virgule fixe mais seulement en utilisant des entiers. Cependant, il convient d'être prudent avec les nombres signés et les grands nombres, on arrive aisément à des dépassements. Sachez aussi que plus vous utilisez un SHL ou SHR avec une opérande grande, plus le résultat sera précis (plus de décimales disponibles).

On aurait pu fort bien faire :

MOV AX,64 ;(0.5*128)
MOV CX,3200
XOR DX,DX
MUL CX
SHR AX,7 ;AX/128

Nous avons simplement une virgule fixe '128x plus grande'.

Retour au sommaire

C - Les nombres négatifs

Dans notre système numérique nous utilisons le signe "-" pour préciser qu'un nombre est négatif. Mais lorsqu'on veut représenter un nombre négatif en base binaire, on ne dispose pas du signe "-". On ne peut utiliser, en effet, que les deux états possibles du bit : 0 ou 1.

La convention utilisée sur PC est celle du complément à 2 d'un nombre. Ainsi, si on veut représenter -3 sur octet, il faudra :

    - calculer la représentation binaire de 3

    - inverser les 8 bits de l'octet

    - ajouter 1 au résultat obtenu

La représentation binaire de 3 sur un octet est la suivante :

00000011

On inverse à présent tous les bits de l'octet :

11111100

Et on ajoute à présent 1 à ce résultat :

11111101

Cet octet vaut donc :

binaire | 1111 | 1101
décimal | 15 | 13
hexadécimal | F | D

De cette manière, dès que le bit le plus significatif (bit de poids le plus fort; c'est le bit 7 sur un octet) vaut 1, c'est que le nombre est négatif.

Ci-joints deux exemples pour mieux comprendre:

      • Représentation, sous forme hexadécimale, un octet contenant le nombre décimal -19.
      représentation binaire de 19 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 1
inversion de tous les bits | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0
addition d'une unité | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1

      Il faut décomposer le résultat obtenu en demi-octet :

      binaire | 1110 | 1101
décimal | 14 | 13
hexadécimal | E | D

      Le nombre -19, codé sur un octet, vaut EDh.

      • Représentation, sous forme hexadécimale, un mot contenant le nombre décimal -19.

      Lorsqu'il faut convertir un mot en double mot, ou un octet en mot, 2 cas peuvent se reproduire : soit le nombre est négatif (bit de poids fort mis à 1), soit il est positif (bit de poids fort mis à 0). Si le nombre est positif les nouveaux bits seront mis à 0; s'il est négatif ils seront mis à 1. Dès lors, il suffit de tester l'état du bit de poids fort.

      La valeur hexadécimale, codée sur un octet, de -19 est EDh; le bit de poids fort est allumé et, donc, tous les nouveaux bits seront mis à 1.

      binaire | 1111 | 1111 | 1110 | 1101
décimal | 15 | 15 | 14 | 13
hexadécimal | F | F | E | D

      Le nombre décimal -19, codé sur un mot, vaut FFEDh.

Retour au sommaire

D - Les instructions logiques

Les instructions logiques manipulent les bits individuellement. Elles font intervenir des opérandes et des emplacements mémoire de 8 ou 16 bits mais les opérations sur ces opérandes se font bit par bit.

a ) AND

L'instruction NAD effectue un ET LOGIQUE sur les 2 opérandes spécifiées, le résultat est placé dans la première opérande. Rappelons qu’un ET LOGIQUE donne le résultat 1 si les 2 opérandes sont 1 et donne 0 dans les autres cas.

Les combinaisons possibles sont donc :
    0 AND 0 = 0
    0 AND 1 = 0
    0 AND 0 = 0
    1 AND 1 = 1
Exemple :
    MOV AX, 29
    MOV BX, 7
    AND AX, BX

    0001 1101 (AX=29)
    AND 0000 0111 (BX=7)
    -------------
    0000 0101 (AX=5)

b ) OR

Cette instruction effectue un OU LOGIQUE INCLUSIF sur les opérandes spécifiées, le résultat est placé dans la première opérande. Un OU LOGIQUE INCLUSIF donne un résultat 0 si les deux opérandes sont 0 et donne 1 dans les autres cas.

Les combinaisons possibles sont donc :

    0 OR 0 = 0
    0 OR 1 = 1
    0 OR 0 = 1
    1 OR 1 = 1

Exemple :
    MOV AX, 0104h

    On souhaite mettre à 1 les bits 2 et 4 sans modifier les autres bits :
    OR AX, 0014h

    00000001 00000100 (AX)
    OR 00000000 00010100 (=0014h)

    ----------------------
    00000001 00010100

c ) XOR

Cette instruction effectue un OU LOGIQUE EXCLUSIF sur les opérandes spécifiées, le résultat est placé dans la première opérande. Un OU LOGIQUE EXCLUSIF donne un résultat 1 si les deux opérandes sont différentes et donne 0 dans les autres cas.

Les combinaisons possibles sont donc :

    0 XOR 0 = 0
    0 XOR 1 = 1
    0 XOR 0 = 1
    1 XOR 1 = 0

Exemple :
    XOR AX, AX (remet à 0 le registre AX)
    XOR BH, BH (remet à 0 le registre BH)
    XOR AH, 6 (crée un masque 00000110 (=6 en décimal) qui sert à complémenter les bits 1 et 2 de AH.)

d ) NOT

Cette instruction effectue un NON LOGIQUE sur l’opérande spécifiée ; le résultat est placé dans l’opérande. Un NON LOGIQUE inverse (complémente) la valeur d’un bit, les 2 combinaisons possibles sont donc :
    NOT 0 = 1
    NOT 1 = 0
Exemple :
    NOT AX (inverse tous les bits de AX)
    NOT BH (inverse tous les bits de BH)

e ) TEST

Cette instruction teste la valeur d’un ou plusieurs bits en effectuant un ET LOGIQUE sur les opérandes. Les opérandes ne sont pas modifiées par l’opération : ce sont les indicateurs qui donnent le résultat du TEST.(cf les instructions conditionnelles)

Exemple :
    TEST AX,1 (effectue un ET LOGIQUE avec AX : si le bit 1 de AX est égale à 1, l’indicateur ZF sera activé.)

Retour au sommaire

6 - La mémoire et ses instructions

Nous allons aborder sa mise en place et voir d'une façon plus précise, la manière de l'utiliser.

Il existe pour cela plusieurs instructions. Nous avons vu que "MOV SI, offset MESSAGE" plaçait dans SI, la valeur de l'offset de MESSAGE. Pour une donnée dans un autre segment, il faut aussi utiliser le "MOV AX,seg MESSAGE" et ensuite "MOV DS,AX". En effet, les registres ES, DS, FS et GS n'acceptent pas de valeur numérique directe mais seulement des registres.

Il existe une autre instruction, LEA, qui permet de placer l'offset dans un registre mais en plus court. Par exemple "LEA SI,MESSAGE" placera dans SI, l'offset de Message.

Privilégiez l'emploi de LEA plutôt que du MOV. Cela rend le code plus clair et plus compact.

Pour pouvoir déplacer des blocs entiers de mémoire, nous n'allons pas lire chaque octet et le placer dans l'autre bloc mais nous emploierons les instructions :

MOVSB
MOVSW
MOVSD

La première pour déplacer une (B)ytes, la deuxième un (W)ord et la dernière pour déplacer un (D)words qui font 32 bits.

Ces instructions demandent que la source (DS:SI) et l'arrivée (ES:DI) soient configurées.

Il ne faut pas diriger les données vers n'importe quel emplacement de la mémoire. Vous planteriez à coup sur le programme. Nous verrons plus loin, comment allouer des blocs de mémoire.

Par exemple dans DS:SI, nous pouvons avoir les données d'une image et dans ES:DI, l'adresse de la mémoire vidéo (A000h:0000). Si nous voulons copier 10000 bytes, nous emploierons directement le MOVSD (si nous travaillons avec un 386 ou plus) ou avec MOVSW.

Pour plusieurs MOVSx, il est inutile de faire une boucle. L'instruction REP est là pour cela.

Il faut utiliser conjointement avec REP, le registre CX qui contient le nombre d'itérations (le nombre de fois que l'on répete le MOVSx).

Pour les 10000 bytes, nous avons :

MOV CX,10000

REP MOVSB

Si nous utilisons les Words (plus rapide), c'est :

MOV CX,5000 (un word=2 bytes)

REP MOVSW

Finalement, avec le 32 bits, c'est :

MOV ECX,2500 (un dword=2 words=4 bytes) - utilisation d'un registre étendu

REP MOVSD

A chaque MOVSx, DI augmente de 1,2 ou 4 bytes.

Maintenant, si nous voulons garder des données ou les placer dans un emplacement de la mémoire ?

Nous utiliserons les instructions :

STOSB

STOSW

STOSD

Comme pour les MOVSx, cette instruction stocke un byte, un word et un dword.

A la différence des déplacements, les STOSx utilisent AL, AX (EAX 32bits) comme valeur à stocker. La valeur de AL,AX,EAX sera stockée dans ES:DI. A chaque STOSx, DI est augmenté de 1,2 ou 4 bytes. Ces instructions sont utiles pour effacer l'écran par exemple ou remplir un bloc mémoire avec la même valeur.

MOV CX,1000

MOV AX,0

REP STOSB

Dans cette exemple, CX équivaut à 1000, pour 1000 répétitions. Ensuite, le MOV AX,0 qui est la valeur qui sera placée dans le bloc mémoire. Pour finir, le REP STOSB qui dit que nous plaçons 10000 bytes de valeur 0 à l'emplacement de ES:DI. En fait, ici nous placons AL, on ne tient pas compte de AH dans un STOSB.

Pour réserver de la mémoire, il faut employer les interruptions, ici nous restons dans la mémoire conventionnelle, c'est à dire que l'on ne dépasse pas les 640ko attribués par le DOS. Si l'on veut plus de mémoire, il faut utiliser les modes dits protégés ou flat. La programmation de ces modes est différentes et il faut avoir quelques notions sur le DOS et la gestion de la mémoire. Si vous voulez dès maintenant en savoir plus, lisez des documentations comme celles d'Intel.

Pour la mémoire conventionnelle, on doit d'abord libérer l'emplacement occupé par notre programme :

MOV BX,taille ; la taille=(taille du prog en octet / 16)+1

MOV AH,04ah

INT 21h

Ici, taille=4096 correspond 64ko. Ajustez donc la taille en fonction de votre programme.

J'ai ajouté 1 à la taille pour éviter des erreurs (normalement pas nécessaire, on ne sait jamais ;).

Ensuite, il faut réserver le bloc de mémoire:

MOV AH,48h
MOV BX,mem_voulue ; (mem_voulue=(mémoire_désirée/16)+1)
INT 21h
JC NOT_ENOUGH_MEM

La mémoire voulue se calcule comme pour la libération de la mémoire. Le JC NOT_ENOUGH_MEMORY est utile pour détecter les erreurs. Si la mémoire n'est pas disponible, votre programme ira au label NOT_.... et fera ce que vous voulez pour indiquer l'erreur (affichage d'un texte, retour au DOS). IMPORTANT, il faut conserver la valeur de AX après avoir exécuté le INT 21h. AX contient le segment du bloc alloué et il faut garder cette valeur pour pouvoir la réutiliser lors du désallouage.

Finalement, il faut désallouer les blocs de mémoire APRES les avoir utilisé (sinon quel intérêt ?).

MOV BX,4096 ; (taille du bloc)

MOV AX,segment_bloc ; (le segment du bloc, nous l'avons précieusement gardé)

MOV ES,AX

MOV AH,49h

INT 21h

Le fait de ne pas désallouer de la mémoire va vous en enlever pour des programmes qui seraient exécutés par la suite.

Retour au sommaire

7 - LES INSTRUCTIONS ASSEMBLEUR

Vous pourrez constater qu’à la section J (consacrée aux sauts : jump), j’y ai introduit des codes hexadécimaux. Ceux-ci sont la représentation des commandes assembleur en hexadécimal, ils nous permettrons de patcher nos programmes.

Pour accéder aux instructions plus rapidement, cliquez sur la lettre désirée :

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z

A :
AAA | ajustement ascii de <AL> après une addition. Format BCD
AAD | ajustement ascii de <AX> avant une division BCD
AAM | ajustement ascii de <AL> après une multiplication. Format BCD
AAS | ajustement ascii de <AL> après une soustraction.
ADC | addition avec retenue <CF>
ADD | addition sans retenue
AND | opération logique ET
ARPL | ajuste le niveau du privilège (mode protégée)
Retour
B :
BOUND | teste un nombre par rapport à des limites
BSF | cherche 1 bit de droite à gauche
BSR | cherche 1 bit de gauche à droite
BSWAP | conversion d'un registre en un format INTEL
BT | charge 1 bit en CF
BTC | charge 1 bit en CF et complément
BTR | charge 1 bit en CF puis le met à 0 dans la source
BTS | charge 1 bit en CF puis le met à 1 dans la source
Retour
C :
CALL | appel de sous programme
CBW | convertit l'octet signé de <AL> en un mot dans <AX>
CDQ | convertit le mot signé de EAX en un quadruple mot dans <EDX:EAX>
CLC | mets l'indicateur à 0 dans CF
CLD | mets l'indicateur à 0 dans DF
CLI | mets l'indicateur à 0 dans IF
CLTS | mets à 0 l'indicateur de changement de contexte Task-Switch-Flag mode protégée.
CMC | complémente l'indicateur CF
CMP | comparaison logique
CMPS[b][w] | compare des chaînes de caractères
CMPSD | compare des chaînes de caractère 80386()
CMPXCHG | compare l'accumulateur avec AL, AX, ou EAX
CWD | convertit le contenu signe de AX en un double mots DX:AX
CWDE | " " " " " "" " "" " " " " " " " "" " " " " " " " dans EAX
Retour
D :
DAA | ajustement décimal de AL après une addition BCD
DAS | ajustement décimal de AL après une soustraction BCD
DEC | décrementation
DIV | division non signée
Retour
E :
ENTER | construit un cadre de pile pour une procédure de haut niveau.
ESC | accès pour le coprocesseur
Retour
H :
HLT | arrêt du processeur en attente d'un événement externe
Retour
I :
IBTS | insère une chaîne de bits (mode protégée)
IDIV | division signée
IMUL | multiplication signée
IN | lit un octet ou mot sur un port périphérique
INC | incrémentation
INS[b][w] | lit une chaîne sur un port
INT | interruption logiciel
INTO | active l'interruption 4 si l'indicateur OF est armé
INBD | efface le contenu de la mem cache du 486
INVLPG | exploitation du 486 en mode virtuel. multiprocesseur 8088
IRET | retour d'interruption
IRETD | retour d'interruption depuis un segment de 32bits
Retour
J :
JA | 0F87 | branchement si supérieur
JAE | 0F83 | branchement si supérieur ou égal
JB | 0F82 | branchement si inférieur
JBE | 0F86 | branchement si inférieur ou égal
JC | 0F82 | branchement si CF est à 1
JNC | 0F83 | branchement si CF est à 0
JCXZ | E3 | branchement si CX ECX est à 0
JECXZ | E3 | branchement si le registre ECX est à 0
JE | 74 | branchement en cas d’égalité (=JZ)
JG | 0F8F | branchement si arithmétiquement supérieur
JGE | 0F8D | branchement si arithmétiquement supérieur ou égal
JL | 0F8C | branchement si arithmétiquement inférieur
JLE | 0F8D | branchement si arithmétiquement inférieur ou égal
JMP | EB | branchement à l'adresse indiquée
JNA | 0F86 | branchement si non supérieur
JNAE | 0F82 | branchement si non supérieur ou égal
JNB | 0F83 | branchement si non inférieur
JNBE | 0F87 | branchement si non inférieur ou égal
JNE | 75 | branchement en cas de non égalité (=JNZ)
JNG | 0F8E | branchement si arithmétiquement non supérieur
JNGE | 0F8C | branchement si arithmétiquement non supérieur ou égal
JNL | 0F8D | branchement si non inférieur arithmétiquement
JNLE | 0F8E | branchement si arithmétiquement non inférieur ou égale
JNO | 0F81 | branchement si l'indicateur OF est à 0
JNP | 0F8B | branchement si parité impaire (indicateur PF à 0)
JNS | 0F89 | branchement si positif
JNZ | 0F85 | branchement si diffèrent (=JNE)
JO | 0F80 | branchement si OF est à 1
JP | 0F8A | branchement si parité pair indicateur PF est à 1
JPE | 0F8A | " " " " " " " " " " " " " " " " " " " " " " " " "
JPO | 0F8B | branchement si parité impair indicateur PF est à 0
JS | 0F88 | branchement si négatif
JZ | 0F84 | branchement en cas égalité (=JE)
Retour
L :
LAHF | charge en AH la partie basse du registre des indicateurs
LAR | charge le droit accès (mode protégée)
LDS | charge une adresse physique en DS: registre
LEA | charge une adresse effective
LEAVE | libère le cadre de pile, installer par entrée
LES | charge une adresse physique en ES: registre
LFS | charge une adresse physique en FS: registre
LGDT | charge le registre de la table des descripteurs globaux (mode protégée)
LGS | charge une adresse physique en GS: registre
LIDT | charge le registre de la table des descripteurs d'interruption (MODE P)
LLDT | charge le registre de la table des descripteurs locaux
LMSW | charge le mot d’état de la machine (mode protégée)
LOCK | verrouille le bus
LODS[b][w] | charge AL/AX avec le contenu de DS:SI
LODSD | charge EAX avec le contenu de DS:SI
LOOP | branchement tant que CX #0
LOOPE | branchement tant que CX #0et ZF =1
LOOPNZ | branchement tant que CX =0
LOOPZ | branchement tant que CX =1
LSL | charge une limite de segment (mode protégée)
LSS | charge une adresse physique en SS: registre
LTR | charge le registre de tache (mode protégée)
Retour
M :
MOV | transfère une donnée
MOVS | transfère une chaîne de caractère octet par octet ou mot pour mot de DS:SI en ES:DI
MOVSD | transfère une chaîne de caractères double mot par double mot
MOVSX | transfert avec extension de signe
MOVZX | transfert avec une extension de 0
MUL | multiplication non signée
Retour
N :
NEG | négation par complément de 2
NOP | pas d'opération
NOT | opération logique NON complément à 1
Retour
O :
OR | opération logique OU inclusif
OUT | transmets un octet ou mot à un periph
OUTS[b][w] | transmets une chaîne à un port
OUTSD | transmets un double mot à un port
Retour
P :
POP | dépile un mot
POPA | dépile les registres
POPAD | dépile tous les registres 32 bits
POPF | dépile un mot et le transfère vers le registre des indicateurs
POPFD | dépile un DOUBLE MOT et le transfère vers le registre des indicateurs sur 32bits
PUSH | empile une valeur
PUSHA | empile tous les registres
PUSHAD | empile tous les registres 32 bits
PUSHF | empile le registre des indicateurs
PUSHFD | empile le registre des indicateurs à 32bits
Retour
R :
RCL | rotation à gauche à travers CF
RCR | rotation à droite à travers CF
REP[z][nez] | préfixes de répétition
REP[e][ne] | pour traiter les chaînes de caractère en association avec CX et les indicateurs
RET[n][f] | retour de sous programme
ROL | rotation à gauche
ROR | rotation à droite
Retour
S :
SAHF | copie AH dans la partie basse du registre des indicateurs.
SAL | décalage à gauche avec introduction de 0
SAR | décalage à droite avec signe
SBB | soustraction non signée avec prise en compte de CF
SCAS[b][w] | compare une chaîne octet par octet ou mot par mot avec le contenu de AL/AX
SCASD | compare une chaîne double mot par double mot avec le contenu EAX
SETA | initialisation à 1 si CF et ZF sont à 0, sinon initialisation à 0
SETAE | initialisation à 1 si CF est à 0, sinon init. à 0
SETB | initialisation à 1 si CF est à 1, sinon init. à 0
SETBE | initialisation à 1 si CF ou ZF est à 1, sinon initialisation à 0
SETE | initialisation à 1 si ZF est à 1, sinon init. à 0
SETG | initialisation à 1 si ZF est à 0 et SF=OF, sinon init. à 0
SETGE | initialisation à 1 si SF=OF, sinon init. à 0
SETL | initialisation à 1 si SF#OF, sinon init. à 0
SETLE | initialisation à 1 si ZF est à 1 et SF#OF, sinon init à 0
SETNA | initialisation à 1 si CF ou ZF est à 1, init à 0
SETNAE | initialisation à 1 si CF est à 1, sinon init. à 0
SETNB | initialisation à 1 si CF est à 0, sinon init à 0
SETNBE | initialisation à 1 si CF et ZF sont à 0, sinon, init. à 0
SETNE | initialisation à 1 si ZF est à 0, sinon init à 0
SETNG | initialisation à 1 si ZF est à 1 ou SF#OF, sinon init. à 0
SETNGE | initialisation à 1 si SF#OF, sinon init. à 0
SETNL | initialisation à 1 si SF et OF sont égaux, sinon init. à 0
SETNLE | initialisation à 1 si ZF est à 0 et SF=OF, sinon init à 0
SETNO | initialisation à 1 si OF est à 0, sinon init à 0
SETNP | initialisation à 1 si PF est à 0, sinon init à 0
SETNS | initialisation à 1 si SF est à 0, sinon init à 0
SETNZ | initialisation à 1 si ZF est à 0, sinon init à 0
SETO | initialisation à 1 si OF est à 1, sinon init à 0
SETP | initialisation à 1 si PF est à 1, sinon init à 0
SETPE | " " " " "" " " " " " " " " " " " " "" "" " "" "
SETPO | initialisation à 1 si PF est à 0, sinon init à 0
SETS | initialisation à 1 si SF est à 1, sinon init à 0
SETZ | initialisation à 1 si ZF est à 1, sinon init à 0
SGDT | sauvegarde le registre de la table des descripteurs globaux(mode prot.)
SHL | voir SAL
SHLD | décalage double à gauche
SHR | décalage à droite avec introduction de 0
SHRD | décalage double à droite
SIDT | sauvegarde le registre de la table des interruptions. (mode protégée)
SLDT | sauvegarde le registre de la table des descripteurs locaux (mode P)
SMSW | sauvegarde le mot d’état de la machine (mode P)
STC | mets à 1 l'indicateur CF
STD | mets à 1 l'indicateur DF
STI | mets à 1 l'indicateur IF
STIOS[b][w] | transfert octet par octet, mot par mot le contenu de AL en ES:DI
STOSD | transfert double mot par double mot le contenu de EAX en ES:DI
STR | sauvegarde le registre de tache (mode Protégée)
SUB | soustraction non signée
Retour
T :
TEST | test si un bit est à 1
Retour
V :
VERR | test l'autorisation de lecture d'un segment (mode protégée)
VERW | test l'autorisation d’écriture dans un segment (mode protégée)
Retour
W :
WAIT | attends que la ligne BUSY ne soit plus actif
Retour
X :
XADD | addition signée
XBINVD | efface le contenu de la mémoire cache du 486
XBTS | prends une chaîne de bits (mode protégée)
XCHG | échange les contenus de 2 registres
XLAT | charge en AL l'octet de la table DS:BX+AL
XOR | opération logique ou exclusive
Retour

Retour au sommaire

8 - Table ASCII

Comme pour les commandes en assembleur, les caractères imprimables ou non imprimables (retour chariot par exemple) ont une correspondance en hexadécimal.

Caractères non imprimables |  |  |  |  | Caractères imprimables
Nom | Ctrlcaract | Dec | Hex | Caract |  | Dec | Hex | Caract | Dec | Hex | Caract | Dec | Hex | Caract
null | Ctrl-@ | 0 | 00 | NUL |  | 32 | 20 | Space | 64 | 40 | @ | 96 | 60 | `
Début d’entête | Ctrl-A | 1 | 01 | SOH |  | 33 | 21 | ! | 65 | 41 | A | 97 | 61 | a
Début de texte | Ctrl-B | 2 | 02 | STX |  | 34 | 22 | " | 66 | 42 | B | 98 | 62 | b
Fin de texte | Ctrl-C | 3 | 03 | ETX |  | 35 | 23 | # | 67 | 43 | C | 99 | 63 | c
end of xmit | Ctrl-D | 4 | 04 | EOT |  | 36 | 24 | $ | 68 | 44 | D | 100 | 64 | d
enquiry | Ctrl-E | 5 | 05 | ENQ |  | 37 | 25 | % | 69 | 45 | E | 101 | 65 | e
acknowledge | Ctrl-F | 6 | 06 | ACK |  | 38 | 26 | & | 70 | 46 | F | 102 | 66 | f
Cloche | Ctrl-G | 7 | 07 | BEL |  | 39 | 27 | ' | 71 | 47 | G | 103 | 67 | g
backspace | Ctrl-H | 8 | 08 | BS |  | 40 | 28 | ( | 72 | 48 | H | 104 | 68 | h
horizontal tab | Ctrl-I | 9 | 09 | HT |  | 41 | 29 | ) | 73 | 49 | I | 105 | 69 | i
line feed | Ctrl-J | 10 | 0A | LF |  | 42 | 2A | * | 74 | 4A | J | 106 | 6A | j
vertical tab | Ctrl-K | 11 | 0B | VT |  | 43 | 2B | + | 75 | 4B | K | 107 | 6B | k
form feed | Ctrl-L | 12 | 0C | FF |  | 44 | 2C | , | 76 | 4C | L | 108 | 6C | l
carriage feed | Ctrl-M | 13 | 0D | CR |  | 45 | 2D | - | 77 | 4D | M | 109 | 6D | m
shift out | Ctrl-N | 14 | 0E | SO |  | 46 | 2E | . | 78 | 4E | N | 110 | 6E | n
shift in | Ctrl-O | 15 | 0F | SI |  | 47 | 2F | / | 79 | 4F | O | 111 | 6F | o
data line escape | Ctrl-P | 16 | 10 | DLE |  | 48 | 30 | 0 | 80 | 50 | P | 112 | 70 | p
device control 1 | Ctrl-Q | 17 | 11 | DC1 |  | 49 | 31 | 1 | 81 | 51 | Q | 113 | 71 | q
device control 2 | Ctrl-R | 18 | 12 | DC2 |  | 50 | 32 | 2 | 82 | 52 | R | 114 | 72 | r
device control 3 | Ctrl-S | 19 | 13 | DC3 |  | 51 | 33 | 3 | 83 | 53 | S | 115 | 73 | s
device control 4 | Ctrl-T | 20 | 14 | DC4 |  | 52 | 34 | 4 | 84 | 54 | T | 116 | 74 | t
neg acknowledge | Ctrl-U | 21 | 15 | NAK |  | 53 | 35 | 5 | 85 | 55 | U | 117 | 75 | u
synchronous idel | Ctrl-V | 22 | 16 | SYN |  | 54 | 36 | 6 | 86 | 56 | V | 118 | 76 | v
end of xmit block | Ctrl-W | 23 | 17 | ETB |  | 55 | 37 | 7 | 87 | 57 | W | 119 | 77 | w
cancel | Ctrl-X | 24 | 18 | CAN |  | 56 | 38 | 8 | 88 | 58 | X | 120 | 78 | x
end of medium | Ctrl-Y | 25 | 19 | EM |  | 57 | 39 | 9 | 89 | 59 | Y | 121 | 79 | y
substitute | Ctrl-Z | 26 | 1A | SUB |  | 58 | 3A | : | 90 | 5A | Z | 122 | 7A | z
escape | Ctrl-[ | 27 | 1B | ESC |  | 59 | 3B | ; | 91 | 5B | [ | 123 | 7B | {
file separator | Ctrl-\ | 28 | 1C | FS |  | 60 | 3C | < | 92 | 5C | \ | 124 | 7C | |
group separator | Ctrl-] | 29 | 1D | GS |  | 61 | 3D | = | 93 | 5D | ] | 125 | 7D | }
record separator | Ctrl-^ | 30 | 1E | RS |  | 62 | 3E | > | 94 | 5E | ^ | 126 | 7E

Nombre de visites depuis le 17/04/2005


Cours du langage Assembleur pour

Cours du langage Assembleur pour les débutants

Présentation de l'assembleur : Vu dans ... http://www.znsoft.fr/Developpement/Asm/

Qu'est ce que l'assembleur - Comment utiliser l'assembleur - Materiels et logiciels nécessaires

Développer et déployer facilement vos sites web

Télécharger gratuitement Office 2013

Héberger votre site web sur des serveurs gratuits

Visual Studio 2013

Télécharger et apprenez gratuitement

Trucs et Astuces

Découvrez Windows 8.1 et télécharger le gratuitement

Sachez que cette initiation ne s'adresse pas seulement aux débutants en programmation, ceux maîtrisant déjà un langage et souhaitant incorporer dans leur programme du code assembleur y trouveront également leur compte. Et pour ceux qui connaissent déjà ce langage, ils pourront se référer aux derniers chapitres traitant de l'optimisation du code, de la programmation graphique, bref, de l'utilisation "avancée" de l'assembleur.

• Qu'est ce que l'assembleur ?

L'assembleur est un langage de programmation (c'est-à-dire un moyen pour l'homme de communiquer avec la machine) de très bas niveau (entendez par là "très près de la machine"). En effet, la plupart des langages de programmation (C/C++, Pascal, Java, etc...) ont pour but de simplifier la tâche du programmeur en lui proposant des instructions "prêtes à l'emploi" pour les tâches habituelles d'un programme. Par exemple, pour afficher un texte à l'écran, en langage C, vous faites tout naturellement : printf("Hello world!\n");

Mais en assembleur, pas de ça ! Vous êtes obligé de comprendre comment ça ce passe au niveau du processeur (sauf si vous utilisez les interruptions du DOS, bien sûr, mais nous verrons cela plus tard...). De plus, en général, il faut beaucoup de lignes de code pour faire pas grand chose... et le temps de programmation en est d'autant plus long. Mais alors, quels sont les avantages de l'assembleur ??? En fait, étant donné que vous programmez directement le processeur, vous pouvez vous-même effectuer des optimisations sur votre code, suivant les cas ; ce que le compilateur ne fait pas. Mais voyons d'abord ce que fait réellement un compilateur.

• Différences entre le compilateur et l'assembleur

Tout microprocesseur contient en lui-même un jeu d'instructions. Ces instructions, très basiques, se résument à une tâche simple, par exemple, "mettre telle valeur dans la mémoire", ou "additionner telle valeur avec telle autre valeur et mettre le résultat dans la mémoire". On est loin du printf du C ! En fait, l'assembleur va convertir un fichier source contenant les instructions du microprocesseur sous forme de mnémoniques anglaises en un fichier exécutable contenant le code numérique binaire de chaque instruction, et donc compréhensible par le microprocesseur. Ouf ! En clair, l'assembleur ne fait que traduire votre fichier source du langage humain vers le langage binaire. Je suis conscient que ceci peut être difficile à comprendre pour un débutant, je vais donc prendre un exemple concret. Voici ce que peut donner du code en assembleur permettant d'additionner 2 et 3 :

mov ax,2
add ax,3

Traduit en langage binaire, il donnera :

101110000000001000000000000001010000001100000000
( B80200050300 en hexadécimal )

On comprend mieux l'intérêt des mnémoniques et de l'assembleur ! Voyez la section sur les systèmes de numérotation si vous ne savez pas ce qu'est le binaire ou l'hexadécimal.

Le compilateur, lui, analyse un fichier source écrit en un langage dit "structuré", et transforme chaque instruction propre au langage en une suite d'instructions machines, donc il convertit le fichier source en programme assembleur, et ce n'est qu'ensuite qu'est produit le fichier exécutable contenant les codes binaires. En fait, le compilateur effectue une étape de plus que l'assembleur, c'est la transformation "fichier source écrit en langage structuré" vers "fichier source écrit en assembleur". C'est justement l'inconvénient du compilateur : la transformation n'est pas toujours aussi bonne que ce qu'elle pourrait. Evidemment, si vous voulez créer un programme qui affiche "Hello, world !", ou qui calcule la somme de deux nombres, l'utilisation de l'assembleur est inutile (même absurde), car même si le compilateur produit un code assembleur moins bon que ce qu'on pourrait faire directement en assembleur, la perte de vitesse lors de l'exécution du programme ne sera pas perceptible... Par contre, si vous voulez créer une application graphique ou un jeu, l'assembleur vous permettra d'obtenir des fonctions graphiques rapides, à condition bien sûr de bien connaître toutes les subtilités de l'assembleur, de manière à produire un code meilleur que le compilateur.

• Confusion entre le langage assembleur et le programme assembleur

En effet, vous avez peut-être remarqué qu'on désigne par le même nom ("assembleur") le langage et le programme traitant les fichiers sources. Dorénavant, on appellerai "assembleur" le programme assembleur, et "asm" le langage assembleur.

• Avantages et inconvénients de l'asm

Si on compare l'asm aux langages de haut niveau, on peut citer comme inconvénients :

- beaucoup de code pour faire peu
- temps de programmation plus long
- bugs plus fréquents
- débogage plus difficile

Mais non, ne partez pas ! Il n'y a pas que des points faibles :

- rapidité d'exécution
- code compact (peu d'octets)
- on peut absolument tout faire
- on peut optimiser les calculs
- euh... on peut dire : "moi, je connais l'asm !!", ça fait toujours bien pour se faire embaucher dans l'informatique ;).

Cette présentation étant faite, voyons maintenant dans quels cas exactement programmer en asm peut être utile, et comment s'y prendre.

Comment et quand utiliser l'assembleur ?

• Intégrer de l'asm dans un programme écrit en C

Comme je l'ai déjà dit, de faire un programme entièrement en asm est particulièrement long et difficile. Par conséquent, dans cette initiation, nous verrons comment intégrer du code asm dans vos programmes écrits en langage C. J'ai choisi le langage C car c'est le plus répondu, de plus si vous vous intéressez à l'assembleur, c'est que vous cherchez la performance, donc vous connaissez sûrement le langage C. Ceci dit, je prends le C juste comme exemple, et quand je parlerais du C, vous pourrez facilement l'adapter à votre langage préféré.

En fait, la meilleure chose à faire pour pouvoir utiliser à la fois l'assembleur et le langage C (ou tout autre langage évolué), c'est de créer des fonctions en asm, de les assembler en tant que fichier objet, d'écrire des fichiers en-têtes (extension .h) pour déclarer vos fonctions au compilateur, puis de linker vos fichiers objets à votre exécutable final. Il existe une autre solution : l'assembleur "inline". Dans ce cas, l'asm et le C sont dans le même fichier source. Mais l'inconvénient, c'est que chaque compilateur établit une syntaxe différente pour le code asm. Et surtout, le compilateur n'est pas prévu pour écrire de l'assembleur, donc la gestion des erreurs est limitée. De plus, la syntaxe est souvent très lourde et complexe. Nous allons donc utiliser la première méthode, et il vous faudra vous procurer un compilateur et un assembleur (voir la section matériel et logiciels nécessaires).

• Dans quels cas utiliser l'asm ?

L'asm pourra vous être utile dans plusieurs cas :

- Si vous avez besoin de vitesse d'exécution. Par exemple, il est très judicieux d'écrire vos routines graphiques (affichage de points, de lignes, de polygones, etc...) en asm. Cela pourra vous faire gagner de nombreuses images par seconde, car vous pourrez alors optimiser au maximum les calculs. Et nous verrons dans la section consacrée à l'optimisation du code que le gain de temps peut être assez conséquent.

- Si vous désirez un code compact, de très petite taille, par exemple pour programmer un virus... mais cela dépasse le cadre de cette initiation.

- Ou tout simplement pour faire quelque chose que votre compilateur ne permet pas. Par exemple, vous serez obligés de passer par l'assembleur pour réaliser un programme TSR (Terminate and Stay Resident), ou pour détourner une interruption (voir les sections suivantes) ; quoique certains compilateurs le font.

Mais je pense que la majorité des personnes qui s'intéressent à l'asm aujourd'hui recherchent la performance, la vitesse, par exemple pour pouvoir programmer un moteur 3D digne de ce nom, ou pour effectuer des calculs conséquents assez rapidement.

Maintenant, avant de commencer à écrire nos premières lignes de codes, voyons ce qu'il est nécessaire de posséder pour pouvoir profiter des joies de l'assembleur...

Materiels et logiciels nécessaires :

• L'assembleur

Il existe de nombreux assembleurs. Les plus connus sont MASM (Macro Assembler) de Microsoft et TASM (Turbo Assembler) de Borland. Mais ce ne sont pas les meilleurs : ils sont payants, et la syntaxe utilisée pour l'asm et horrible et illogique. Il existe aussi de nombreux assembleurs gratuits. Celui que nous allons utiliser lors de cette initiation est NASM ( Netwide Assembler ) . Il est complet (instructions Pentium et MMX), gratuit, bien documenté (y compris la documentation de toutes les instructions supportées), et possède une syntaxe logique et agréable à lire. Le seul hic, c'est qu'il n'est pas encore en version finale : la dernière version est la 0.98. Mais il est parfaitement utilisable et ne possède pas ou très peu de bugs. Téléchargez-le sans plus attendre !

• Le linker

L'assembleur ne créé pas un fichier exécutable (.exe) directement, mais produit un fichier objet. Un fichier objet contient des instructions assemblées, et le nom des symboles (fonctions, variables, etc...) utilisés. Ce fichier n'est pas exécutable directement. C'est le linker qui va se charger de convertir le fichier objet en fichier exécutable. Mais alors quel est son intérêt ? Pourquoi l'assembleur ne produit pas directement un fichier exécutable ? C'est parce-que le linker peut aussi combiner plusieurs fichiers objets (pas obligatoirement créés avec le même assembleur) pour former un seul fichier exécutable. Cela vous permet d'inclure dans vos programmes des librairies, c'est-à-dire un ensemble de fonctions externes au programme (par exemple il existe des librairies qui s'occupent du son, du joystick, de l'affichage en 3 dimensions, etc...) qui ont été crées avec un assembleur différent, ou même avec un compilateur. Cela vous permet aussi de séparer votre programme en plusieurs fichiers sources (ce qui est vivement conseillé pour des gros programmes), ce qui vous permet de mieux vous y retrouver, et aussi d'accélérer les temps d'assemblage (seuls les fichiers sources que vous avez modifiés sont assemblés). Si vous n'avez pas compris ce que je viens de raconter, ne vous inquiétez pas, vous comprendrez plus tard par la pratique.

Comme nous allons programmer en 32 bits, il nous faut un linker qui supporte le mode protégé ). Le 32 bits n'est pas à l'origine supporté par DOS, il faut donc utiliser un DOS-EXTENDER : c'est un programme qui permet de lancer des exécutables fonctionnant en mode protégé (donc en 32 bits) sous DOS. Le plus simple est d'utiliser l'environnement DJGPP : c'est le portage du compilateur GCC du monde Linux pour DOS, créé par DJ Delorie. Il contient un compilateur C et plein d'autres d'outils très utiles, dont un linker 32 bits, un DOS-EXTENDER, un débogueur (sert à trouver les erreurs dans les programmes, en l'exécutant pas à pas), un programme "make" (vous tapez "make", et le programme se charge d'assembler uniquement les fichiers sources modifiés, puis de linker le tout ; mais avant il faut créer un script indiquant les actions à effectuer : voyez la documentation), et même un assembleur (en fait il est utilisé en interne par le compilateur, on ne peut pas l'utiliser pour programmer en asm directement car il possède une syntaxe plus qu'horrible (syntaxe AT&T) et ne possède pas de documentation). Mais surtout, il fait partie du projet GNU, c'est-à-dire qu'il est gratuit et est livré avec ses sources.

En téléchargement :

Si vous voulez des versions à jour, allez sur le site officiel de DJGPP : www.delorie.com/djgpp . Il vous faudra télécharger :

le linker : bnuXXXb.zip
le serveur DPMI : csdpmiXb.zip
le compilateur C : gccXXXb.zip
le kit de développement : djdevXXX.zip
texinfo : txiXXXb.zip
la FAQ : frfaqXXb.zip

• Un éditeur de texte

N'importe quel éditeur de texte peut faire l'affaire, même l'edit du DOS ou le bloc-notes de Windows.

• Côté matériel

Il vous faut simplement un PC muni d'un processeur Intel 386 ou supérieur. Vous devriez avoir ça... :) Ne vous inquiétez pas : un programme écrit pour le 386 fonctionnera sans problème sur un Pentium ; par contre l'inverse n'est pas vrai. En effet, chaque nouvelle génération de processeur apporte de nouvelles instructions, mais ne modifie pas l'usage des instructions qui existaient déjà (heureusement d'ailleurs).


Introduction au langage 'assembleur' (Intel en particulier)

Introduction au langage "assembleur" (Intel en particulier)

Table des matières Plier Déplier

  • I. Introduction
  • II. Commençons par le vocabulaire
  • III. Liste des registres
  • IV. Liste des drapeaux
  • V. Liste des instructions
  • VI. Liste des interruptions
  • VII. Exemples concrets
    • VII-A. Lecture d'une touche au clavier
    • VII-B. Effacement de l'écran dans le mode VGA (320x200 pixels en 256 couleurs)

J'ai écris cette « introduction » à l'assembleur assez rapidement, et croyant que vous vous y connaissiez un peu dans ce domaine. Alors si vous trouvez une erreur dans ce cours, ou quelque chose vous est incompréhensible, écrivez-moi : vous m'aiderez pour mon cours, et vous aiderez les prochains lecteurs de ce cours.

L'auteur

haypo Profil Pro

I. Introduction

Le processeur d'un ordinateur ne comprend aucun langage de programmation ni Turbo Pascal, ni assembleur, ni même C++ ou Java (du moins aucun des processeurs compatibles Intel 80x86). Il ne connait qu'une chose : le langage machine. C'est une liste de nombre 8 bits en hexadécimal du style : "B0h 12h", pas très parlant. L'assembleur c'est la version « humaine » du code machine, pour notre exemple celà donne : « mov al, 12h » (comprendre « copie la valeur 12h dans le registre AL »). Comme vous pouvez le constater, c'est le langage de programmation le plus proche du processeur (mise à part si vous arrivez à programmer en langage machine).

Question qui vous saute à l'esprit : à quoi ça sert ? L'intérêt premier est la vitesse, car c'est le langage avec lequel on peut faire les programmes les plus rapides. Le deuxième intérêt, qui n'est peut-être plus d'actualité aujourd'hui est l'accès aux interruptions DOS qui permettent un accès direct au matériel tel la souris, l'écran, ou la carte vidéo. Certains vétérants de la programmation n'utilisent que l'assembleur, certains pirates en particulier. Pourquoi ? Pour faire les plus petits programmes possibles, car le compileur ne fait que traduire l'assembleur en code machine (ce qu'on pourrait presque faire manuellement avec de l'entrainement). Et en même savoir exactement ce que contiendra notre fichier binaire (fichier exécutable, « .exe » ou « .com » sur les plate-formes Microsoft).

Personellement, je vous déconseille vivement du faire du 100% ASM (ASseMbleur), car on se perd vite dans le code source (qui atteind souvent plusieurs milliers de lignes car chaque instruction prend une ligne), et ce n'est pas très parlant comme langage. Je vous le recommande (uniquement) pour optimiser des fonctions déjà existantes dans votre langage de prédilection (Pascal, C++...) demandant une vitesse optimale, souvent pour les fonctions les plus souvent appelées ; tracé de ligne, copie d'un bloc mémoire, etc.

II. Commençons par le vocabulaire

Compilateur - Hexadécimal - Bit - Octet - Mot - Double-mot - Segment et offset - Interruption - Pile - Flags

  • Compilateur : Programme traduisant un code source (fichier texte, pour l'assembleur : fichiers portant l'extension « .asm ») en code machine, c'est à dire produisant un fichier binaire (ou fichier « exécutable », portant l'extension « .exe » ou « .com » sur les plate-formes Microsoft).
  • Hexadécimal : Dans la notation des nombres actuelle, on utilise les chiffres arabes compris entre 0 et 9, on a donc 10 chiffres, et le chiffre le plus grand est 9 (10-1). On dit de ce système de notation qu'il est en base 10 (décimal, déci = 10 en grec). Pour écrire 451 par exemple, on peut le décomposer comme 4*100 +5*10 +1*1 = 4*(10^2) +5*(10^1) + 1*(10^0).
    Le système hexadécimal au contraire est en base 16. Pour écrire 10 on utilise la lettre « a », 11 : « b », 12 : « c », 13 : « d », 14 : « e », et 15 : « f ». Donc par exemple 20 (16*1+4) s'écrira 14h. « h » est le suffixe des nombres hexadécimaux en assembleur, en Turbo Pascal on utilise le préfixe « $ » ($14) et en C le préfixe « 0x » (0x14). Pour convertir de l'hexadécimal en décimal, on procède à sa décomposition : 3DAh = 3*(16^2) +D*(16^1) +A*(16^0) = 3*256 +13*16 + 10*1 = 986.
    Remarque: Le processeur central (CPU : Central Processor Unit) travaille uniquement en binaire (uniquement avec bits), notation en base 2. Par exemple 9 s'écrira 1*8 +1 = 1*(2^3) +0*(2^2) +0*(2^1) +1*(2^0) = « 1001b » (« b » étant le suffixe des nombres binaires). Même le coprocesseur arithmétique qui calcule le cosinus et l'exposantiel travaille en binaire !
  • Bit :Un bit est une donnée pouvant prendre soit la valeur 0 (assimilée à false, faux en anglais) ou 1 (true, vrai en anglais). C'est l'unité du système binaire (voir la remarque de l'hexadécimal), un octet par exemple est un groupement de 8 bits.
  • Octet : Un octet est une suite de 8 bits. S'il est signé (+ ou -), il peut prendre une valeur comprise entre -128 et +127 (128 = 2 puissance (8-1) et 127 = 2 puissance (8-1) -1), car le premier bit est celui du signe (TRUE = « - ») et donc la valeur est en 7 bits. S'il n'est pas signé, il peut prendre une valeur comprise entre 0 et +255 (255 = 2^8 -1, « -1 » car le zéro est une valeur à part entière !).
  • Mot : Deux octets = 16 bits. Signé : valeurs entre -32768 (2^15) et +32767 (2^15 -1). Non signé : entre 0 à +65535 (2^16 -1).
  • Double-mot : Quatre octets = 2 doubles mots = 32 bits. Signé : valeurs de -2 147 483 648 (-2^31 = -2 147 millions) et +2 147 483 647 (2^31 -1), non-signé : entre 0 et +4 294 967 295 (quelques 4 294 millions).
  • Registre : Espace mémoire placé physiquement dans le coeur du processeur dans lequel sont stockés des valeurs. Les registres étaient au début codés sur 8 bits (un octet), ils étaient nommés « ?L » (où "?" peut être A,B,C ou D), puis ils sont passés en 16 bits (un mot) et deviennent « ?X ». Le truc magique est que si on modifie « ?L », on modifie la "partie basse" du mot « ?X », c'est à dire « ?X » est en faite composé d'un octet et de « ?L ». Puis avec l'arrivé du 386, on passe en 32 bits (double-mot) et les registres deviennent « E?X », là aussi, si on modifie « ?L » ou « ?X » on modifie une partie de « E?X ».
    Consultez la Liste des registres.
  • Segment et Offset : Les adresses mémoires (emplacement des octets dans les barrettes de mémoire) sont définies par deux registres : le segment (partie haute de l'adresse) et l'offset (partie basse de l'offset). On note l'adresse : « segment :[ offset ] » (« : » est le séparateur, les crochets « [ » et « ] » ne sont pas obligatoires), exemple: « DS:[DI] ». Avec le 386, les offsets sont passés en 32 bits pour accéder à plus de mémoire (plus de 16 Mo), les offsets avec le préfixe « E » sont apparus (ESI, EDI, ESP, EBP), et comme pours les registres 32 bits, les registres 16 bits, les offsets SI,DI,SP,BP en sont leur partie basse.
    Consultez la Liste des registres.
  • Interruption : Une interruption est comme un petit programme stocké en mémoire qui est appelé plus ou moins régulièrement et qui a une tâche spécifique. Il y en a 256 au maximum, et les premières sont intégrées dans le BIOS (programme lancé au démarrage de l'ordinateur qui gère le matériel : disque dur, accès mémoire...) comme les interruptions de la carte vidéo ou le clavier. Certaines sont appelées à une fréquence constance : l'interruption 1Ch par exemple qui est un compteur qui incrémente (ajoute 1) une valeur 18.6 fois/seconde. D'autres sont appelées uniquement si on a besoin d'elle : l'interruption 09h du clavier est appelée à chaque pression ou relâchement d'une touche.
    Voir la liste complète des interruptions.
  • Pile : La pile pourrait être comparée à une pile d'assiettes, on entasse des assiettes (ouais bon, c'est une image) avec l'instruction "PUSH" puis on les enlêve avec l'instruction "POP". La hauteur de la pile est la valeur du registre [E]BP. On voir ce qu'il y a à l'étage X avec une instruction dans le style « MOV AX,[BP-4] », mais pas l'enlever, car si on enlève une assiette au milieu de la pile, la pile dégringole (= plantage du PC).
  • Flags (drapeaux) : Ceux sont des bits internes au processeur.
    Consultez la Liste des drapeaux.

III. Liste des registres

Il existe plusieurs registres ayant un sens plus ou moins précis (ils peuvent avoir une utilisation différente) :

  • AL/AH/EAX : Registre général, sa valeur change très vite.
  • BL/BH/EBX :Registre général, peut servir d'offset mémoire (exemple : « mov al, byte ptr ds:[bx+10] »).
  • CL/CH/ECX : Sert en général de compteur pour les boucles (exemple : « mov ecx, 5 ; rep movsd » : copie 5 doubles mots).
  • DL/DH/EDX :Registre général, obligatoire pour l'accès aux ports (moyen de communiquer avec toutes les puces de l'ordinateur, par exemple les ports 42h et 43h servent à contrôler le haut-parleur interne, voir IN et OUT).
  • CS : Segment mémoire du code.
  • DS : Segment mémoire des données.
  • ES : Segment mémoire.
  • FS : Autre segment mémoire.
  • GS : Autre segment mémoire.
  • SS : Segment mémoire de la pile (« S » = Stack = Pile).
  • BP : Offset mémoire, très souvent une copie de SP à laquelle on soustrait une valeur pour lire dans la pile (on ne doit pas modifier SP).
  • EDI/DI : Offset mémoire utilisé avec ES (ou FS ou GS si spécifié, exemple : « mov al, byte ptr gs:[10] »).
  • EIP/IP : Offset mémoire du code (inaccessible directement, modifiable indirectement avec l'instruction CALL, JMP, ou J[cas]).
  • ESI/SI : Offset mémoire utilisé avec DS.
  • ESP/S : Offset mémoire de la pile.

IV. Liste des drapeaux

  • AF : Auxilliary Flag = Indicateur de retenue auxilliaire.
  • CF : Carry Flag = Indicateur de retenue.
  • DR : Dirrection Flag = Indicateur de direction de traitement des chaînes de caractères.
  • IF : Interrupt Flag = Indicateur d'exécution des interruptions dites « masquables ».
  • OF : Overflow Flag = Indicateur de débordement.
  • PF : Parity Flag = Indicateur de parité -> PF=0 : Impaire, PF=1 : Paire.
  • SF : Sign Flag = Indicateur de signe -> SF=0 : Positif, SF=1 : Négatif.
  • TF : Single Step Flag = Indicateur de débogage.
  • ZF : Zero Flag = Indique une valeur nulle.

V. Liste des instructions

Les caractères « { » et « } » délimitent un paramètre. Il peut être un registre, une zone mémoire ou une immédiate (une valeur, la plupard du temps c'est un nombre).

Le suffixe « [B/W/D] » terminant une instruction doit être remplacé soit par B, soit par W ou soit par D :

  • « B » signifit que l'instruction est en 8 bits (utilise si nécéssaire le registre AL).
  • « W » signifit que l'instruction est en 16 bits (utilise si nécessaire AX).
  • « D » signifit que l'instruction est en 32 bits (utilise si nécessaire EAX).

AND - CALL - CMP - CMPSx - IN - IRET - JMP - J[cas] - LEA - LDS - LES - LFS - LGS - LSS - LODSx - MOV - MOVSx - MOVZX - MUL - NOT - OUT - PUSH - POP - RET - REP - SHL - SHR - STOSx - SCASx - TEST - XOR

  • AND {destination},{masque} : Applique un "et" à {destination} par {masque}. Tout bit de {destination} est mis à 0 si le bit correspondant de {masque} vaut 0, et est inchangé si le bit correspondant vaut 1 :
    0 AND 0 -> 0
    0 AND 1 -> 0
    1 AND 0 -> 0
    1 AND 1 -> 1

    Equivalant en Turbo Pascal : "{destination} := {destination} and {masque}".
    Equivalant en C : "{destion} &= {masque}".

    Voir aussi NOT, OR et XOR.
  • CALL {adresse} : Appelle une procédure qui est à l'adresse {adresse}. Si une ou plusieurs instructions PUSH précèdent un CALL, ceux sont des paramètres qui sont stockés dans la pile. Dans ce cas la procédure commence par "push [e]bp ; mov [e]bp, [e]sp" et on peut trouver la lecture des paramètres avec des instructions du genre "mov {registre},[ebp-{valeur}]". Il ne faudra surtout pas oublier le "RET [valeur]" à la fin de la procédure (voir plus bas).
  • CMP {a}, {b} : Compare les deux variables {a} et {b}. Toujours suivit d'un saut conditionnel ("J[cas] {offset}", voir sa signification plus bas).
  • CMPS[B/D/W] : Compare l'octet/le mot/le double-mot DS:ESI à ES:EDI.
  • IN {destination},{port} : Lit une valeur 8 bits sur le port {port} (16 bits) et la stocke dans {destination}. Le seul registre autorisé pour {port} est DX. Voir aussi OUT.
  • IRET {valeur} : Quitte l'interruption. Est uniquement utilisé pour les programmes résident comme le pilote de la souris par exemple. Voir aussi RET.
  • JMP {offset} : Va ("J" = Jump : Sauter) à l'adresse {offset}.
  • J[cas] {offset} : Va à l'adresse {offset} si la condition [cas] est exacte. [cas] est une condition relative aux "flags" (drapeaux), elle doit être traduite par « Si le résultat d'une opération logique ... » (je prends comme exemple "CMP {a},{b}") :
non signé | JA | est supérieur (a > b), si CF=ZF=0.
 | JAE ou JNB ou JNC | est supérieur ou égal (a => b), si CF=0.
 | JB ou JC | est inférieur (a <b), si CF=1.
 | JBE | est inférieur ou égal (a <= b), si CF=ZF=1.
signé | JG | est supérieur (a > b), si SF=ZF=0.
 | JGE | est supérieur ou égal (a => b), si SF=OF.
 | JL | est inférieur (a < b), si SF<>OF.
 | JLE | est inférieur ou égal (a <= b), si SF<>OF et ZF=1.
égalité | JE ou JZ | est égal (a = b), si ZF = 1.
 | JNE ou JNZ | est différent (a <> b), si ZF = 0.
  • LEA {destination},{source} : Ecrit l'adresse de {source} dans {destination}. Equivaut à "MOV {destination}, OFFSET {source}".
  • LDS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre DS, son segment, et dans {destination} (16 bits), son offset.
  • LES {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre ES, son segment, et dans {destination} (16 bits), son offset.
  • LFS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre FS, son segment, et dans {destination} (16 bits), son offset.
  • LGS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre GS, son segment, et dans {destination} (16 bits), son offset.
  • LSS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre SS, son segment, et dans {destination} (16 bits), son offset.
  • LODS[B/D/W] : Copie l'octet/le mot/le double-mot ES:EDI dans AL/AX/EAX (instruction inverse de STOS[B/W/D]).
  • MOV {dst},{src} : Copie la valeur {src} dans {dst}.
  • MOVS[B/D/W] : Copie l'octet/le mot/le double-mot DS:ESI dans ES:EDI.
  • MOVZX {dst},{src} : Etend à 32 bits le nombre contenu dans {src} (8 bits) et transfert le résultat dans {dst} (16 ou 32 bits).
    Exemple: MOVZX ax,al : Efface la partie haute de AX (AH).
  • MUL {source} : Multiplie la destination explicite par {source}, les deux nombres sont considérés comme non signés. Utiliser "JC {adresse}" pour tester le débordement. La destination est fonction de la taille de source :
    • 8 bits : la destination est AX (le multiplicateur est AL)
    • 16 bits : la destination est DX:AX, c'est à dire AX contient la partie basse et DX la partie haute (le multiplicateur est AX)
    • 32 bits : la destination est EDX:EAX, c'est à dire EAX contient la partie basse et EDX la partie haute (le multiplicateur est EAX)
  • NOT {destination} : Inverse les bits de {destination} :

    NOT 1 -> 0
    NOT 0 -> 1

    Equivalant en Turbo Pascal : "{destination} := not {destination}".
    Equivalant en C : "{destion} = ~{destination}".

    Voir aussi AND, OR et XOR.
  • OR {destination}, {masque} : Applique un "OU logique" à {destination} par {masque}. Tout bit de {destination} est mis à 1 si le bit correspondant de {masque} vaut 1, et laissé inchangé si le bit correspondant vaut 0.

    0 OR 0 -> 0
    0 OR 1 -> 1
    1 OR 0 -> 1
    1 OR 1 -> 1

    Equivalant en Turbo Pascal : "{destination} := {destination} or {masque};".
    Equivalant en C : "{destion} |= {masque}".

    Voir aussi AND, NOT et XOR.
  • OUT {source},{port} : Ecrit la valeur {source} (8 bits) sur le port {port} (16 bits). Le seul registre autorisé pour {port} est DX. Voir aussi IN.
  • PUSH {valeur} : Met une [valeur] dans la pile.
  • POP {registre} : Sort une valeur de la pile et la stocke dans un [registre].
  • REP {instruction}: Répète l'instruction [instuction] ECX fois.
  • RET {valeur} : Quitte la procédure en cours. Si des paramètres ont été envoyés au CALL, [xxxx] est le nombre d'octets envoyés qui sont à sortir de la pile. Voir aussi IRET.
  • SHL {registre},{valeur} : Décalage binaire du {registre} de {valeur} vers la gauche (L = Left), les bits apparaissant à droite sont complétés par des zéros. Exemple : "mov al, 3; shl al,2" donne al = 12, car 3 = 0011 et son décalage 1100. En fait décaler de X revient à multiplier par 2^X (3*2^2 = 3*4 = 3*2*2 = 12).
  • SHR {registre},{valeur} : Décalage binaire du {registre} de {valeur} vers la droite (R = Right), les bits apparaissant à gauche sont complétés par des zéros. Exemple : "mov al, 12; shr al,2" donne al = 3, car 12 = 1100 et son décalage 0011. En fait décaler de X revient à diviser par 2^X (12/(2^2) = 12/4 = 3).
  • STOS[B/D/W] : Copie AL/AX/EAX dans l'octet/le mot/le double-mot ES:EDI (inverse de LODS[B/W/D]).
  • SCAS[B/D/W] : Compare AL/AX/EAX à l'octet/le mot/le double-mot ES:EDI (permet de rechercher une valeur dans une chaine de caractères).
  • TEST {source},{masque} : Teste si les bits {masque} de {source} sont posés ou non, et modifie ZF en conséquence (ZF posé si les bits de {source} sont posés, sinon ZF=0), ce qui sera exploitable avec "JZ" ou "JNZ" par la suite. L'instruction permet de tester un bit particulier de {source}.
    En particulier : TEST {a},{a} = Teste si la variable {a} est à zéro (pose ou non le drapeau ZF).
  • XOR {destination},{masque} : Applique un "ou exclusif" à {destination} par {masque}. Tout bit de {destination} est mis à 1 s'il diffère du bit correspondant de {masque}, et est mis à 0 s'il a la même valeur :
    0 XOR 0 -> 0
    0 XOR 1 -> 1
    1 XOR 0 -> 1
    1 XOR 1 -> 0
    (c'est le même système que pour la multiplication de nombres relatifs : (+2)x(-3) = (-6), et (-3)x(-3)=9 par exemple)

    XOR est utilisé en cryptographique car appliquer deux fois XOR à un même nombre avec le même "masque" redonne le nombre. Exemple :
    24 xor 3 -> 27
    27 xor 3 -> 27

    XOR {a},{a} : Met à la variable {a} à zéro, beaucoup plus rapidement que "MOV {a},0" car XOR est une instruction de base du processeur.

    Equivalant en Turbo Pascal : "{destination} := {destination} xor {masque}".
    Equivalant en C : "{destion} ^= {masque}".

    Voir aussi AND, NOT et OR.

VI. Liste des interruptions

J'ai trouvé des informations par-ci, par-là, donc ne me demandez pas de tout détailler, je n'en sais pas plus.

  • 01h: Lancement d'un programme en mode pas-à-pas (permet de le débogueur).
  • 02h: Interruption non masquable.
  • 03h: Erreur de rupture.
  • 04h: Erreur de dépassement (par calcul, exemple : "mov al, 200 ; add al,140" -> 340 > 255 !!!).
  • 05h "Imprime écran", imprime une copie de l'écran en mode texte.
  • 08h: Horloge tournant à 18.6 clics/seconde.
  • 09h: Lecture du clavier. La touche est codée avec un 'code clavier' (scan code en anglais) traduit en code standart ASCII par l'interruption 16h.
  • 0Bh: Gestion du port COM2.
  • 0Ch: Gestion du port COM1.
  • 10h: Gestion de la carte vidéo.
  • 11h : Liste de configuration (mémoire, nombre de ports COM, co-processeur, ...).
  • 12h: Taille de la mémoire basse (640 Ko maximum).
  • 13h: Gestion des différents disques.
  • 14h: Gestion de l'interface série (ports COM, voir les interruptions 0Bh et 0Ch).
  • 15h: Manette de jeu, cassette et TopView.
  • 16h: Conversion du code de la touche (lu par l'interruption 09h) en code standart ASCII.
  • 17h: Gestion de l'imprimante.
  • 18h: Rom BASIC.
  • 19h: Rountine de chargement du DOS.
  • 1Ah: Gestion de l'heure réelle.
  • 1Bh: Surveille la pression de la combinaison de touche 'CTRL + C'.
  • 1Ch: Chronomètre clic/clic à la vitesse de l'horloge 08h : 18.6 Hz. Sa valeur est stockée à l'emplacement 0040h: 0060h.
  • 1Dh: La table d'initialisation vidéo.
  • 1Eh: La table de paramètre des disquettes.
  • 1Fh: La table des caractères graphiques.
  • 20h: L'interruption DOS : Fin d'un programme au format COM (le format EXE est largement plus répendu aujourd'hui).
  • 21h: L'interruption DOS : Fonctions universelles (disque dur, horloge, ...).
  • 22h: L'interruption DOS : Adresse de fin de processus.
  • 23h: L'interruption DOS : Surveille CTRL + PAUSE (ou CTRL + BREAK).
  • 24h: L'interruption DOS : Erreur fatale d'un vecteur d'interruption.
  • 25h: L'interruption DOS : Lecture directe d'un disque.
  • 26h: L'interruption DOS : Ecriture directe sur un disque.
  • 27h: L'interruption DOS : Programmes résidents.
  • 28h: Fin d'un programme restant résidant en mémoire.
  • 2Fh: Interruption pour plusieurs sous programmes. Gestion du réseau, driver CD-Rom MSCDEX, ...

VII. Exemples concrets

VII-A. Lecture d'une touche au clavier

Principe : l'interruption 16h gère le clavier. Elle possède deux fonctions intéressantes : 00h, lecture d'une touche; et 01h, vérification de la présence d'une touche dans le tampon clavier. Mais si une touche est étendue, par exemple les touches fléchées, ou les touches "page haut", "insertion", etc., la fonction 00h nous reverra un code null (00h) comme code ASCII, puis le code ASCII de la touche étendue. Sachant que le code ASCII est toujours inférieur à 128, on pourra ajouter 128 aux codes étendus pour au final n'avoir à appeler notre future fonction de lecture d'une touche qu'une seule fois.

Solution Turbo Pascal
(uses Dos;)

const MasqueFlagsCF = 1; { Masque pour isoler le bit CF des flags } (1)

function TouchPresse : Boolean;
var Regs: Registers; (2)
begin with Regs do begin
&#160;AH := $01; { Fonction 01h: Vérification de la présence d'une touche }
&#160;Intr ($16,Regs); (3)
&#160;TouchPresse := (Flags and MasqueFlagsCF = MasqueFlagsCF); (1)
end end;

function LitTouche : Char;
var Regs: Registers;
begin with Regs do begin
&#160;AH := $00; { Fonction 00h : Lecture d'une touche }
&#160;Intr ($16,Regs);
&#160;if AL=0 then (4)
&#160;&#160;LitTouche := Char(AH or 128) (5)
&#160;else
&#160;&#160;LitTouche := Char(AL);
end end;

Analyse :

  • (1) - Le type Registre (2) nous met à disposition les Flags, met dans un mot d'ensemble (Word). Pour isoler le flag CF (bit #0), on utilisera le truc : "if Flags and MASQUE = MASQUE" où MASQUE est une puissance de 2, la puissance est la position du bit (en partant de zéro). CF étant à la position 0 (bit #0), le MASQUE est donc 1 (2^0).
  • (2) - Pour appeler une interruption, Turbo Pascal nous met à disposition (par l'unité DOS) le type "Registers" qui permet d'accéder de façon virtuelle aux registres. Virtuelle car celà ne modifie par directement les registres, les registres sont tous envoyés comme "paramètre", puis sont lus après l'appelle de "Intr" (3) (appelle d'une l'interruption).
  • (3) - Enfin pour appeler une interruption, on utilise la fonction "Intr" avec comme paramètre le numéro de l'interruption et les registres. Ici on utilise la fonction 00h de l'interruption 16h. Celle-ci retourne dans AL le code ASCII de la touche et AH contient le code étendu de la touche (si AL=0).
  • (4) - Pour savoir si une touche est étendue, on vérifie que AL = #0.
  • (5) - Pour ajouter 128 à la valeur de la touche étendue, on peut également faire "OR 128", celà pose dans tous les cas le bit #7 de AH. Ca ne fonctionne que pour les puissances de 2 (1, 2, 4, 8, 16, 32, 64, 128, 256, ...).
Solution Assembleur

function TouchPresse : boolean; assembler; asm
&#160;mov ah,01h { Fonction 01h = Vérifit qu'une touche soit disponnible }
&#160;int 16h { Appel l'interruption clavier }
&#160;mov al,1 { Une touche est présente (TRUE) }
&#160;jnz @PasVide (1)
&#160;xor al,al { Pas de touche touche (FALSE) }
@PasVide:
end;

function LitTouche : char; assembler; asm
&#160; xor ah,ah { Fonction 00h = Lit une touche du clavier }
&#160; int 16h { Appel l'interruption clavier }
&#160; or al,al { Est-ce une touche étendue? (AL=0) } (2)
&#160; jz @Etendue { Ouais -> AL = code étendu (AH) + 128 } (2)
&#160; ret { Touche standart -> on se tire }

@Etendue:
&#160; mov al,ah { AL = Code ASCII étendu }
&#160;&#160;or al,128 { Ajoute 128 à celui-ci pour le distinguer }
end;

Analyse :

  • (1) - La fonction 01h de l'interruption 16h renvoi la présence d'une touche par le flag ZF. Pour le tester on utilise JNZ : ZF=0, ne fait rien; ZF=1 : Saute !
  • (2) - La fonction 00h de l'interruption 16h renvoi le code ASCII de la touche dans le registre AL. Si celui-ci vaut 0, alors la touche est étendue, et le code est stocké dans AH. Pour savoir si AL=0, on peut faire "cmp al,0; jz @Saut", mais il est plus rapide de faire le test par "or". Celui-ci modifie le flag ZF : Si AL=0, ZF=1 (ZF = Zero Flag !); si AL<>0, ZF=0.

VII-B. Effacement de l'écran dans le mode VGA (320x200 pixels en 256 couleurs)

Dans le mode VGA, la mémoire vidéo est placée à l'adresse $A000:$0000, et prend 320*200=64 000 octets. Le but du jeu est de remplir ces 64 000 octets avec une couleur précise, c'est à dire un valeur codée sur un octet.

Solution en Turbo Pascal
Sélectionnez
1.
fillchar (Mem[$A000:0],64000,Couleur); { Couleur étant un "Byte", un octet }

Celà fonctionne très bien, mais Turbo Pascal étant développé pour fonctionner avec les 286, processeur en 16 bits, fillchar fonctionnera donc en 16 bits (voir en 8 bits, je ne sais pas trop). On va donc l'optimiser en passant écrivant dans la mémoire avec les instructions 32 bits.

Solution Assembleur (intégrée dans un programme Turbo Pascal, première version, dite "éducative")

rocedure EffaceEcran (Coul: byte); begin
asm (1)
mov ax,0A000h (2)
mov es,ax { ES = A000h }
mov di,0 { Maintenant ES:DI pointe sur l'adresse A000:0000 }
mov al,[coul] { On lit la couleur passée en paramètre } (3)
mov cx,64000 { 64 000 octets à remplir}
rep stosb { Ecrit CX fois l'octet AL à l'adresse ES:DI }
end; (1)
end;

Analyse :

  • (1) - En turbo Pascal, on peut intégrer de l'assembleur n'importe où en tapant "asm { les instructions } end;".
  • (2) - Les nombres hexadécimaux débutants par des lettres peuvent être confondus avec des noms de variables, il faut alors rajouter un zéro en préfixe.
  • (3) - Pour lire les paramètres, rien de plus simple : taper son nom (les crochets ne sont pas obligatoires).
Solution Assembleur (version optimisée 32 bits)

Procedure EffaceEcran (Coul: byte); Assembler; asm (1)
mov ax,0A000h
mov es,ax
{ xor edi, edi } (2) db 66h; xor di,di (3) { ES:DI pointe sur l'adresse A000:0000 }
mov al,[coul] { On lit la couleur passée en paramètre }
mov ah, al { Copie AL dans AH, donc AX = deux fois la couleur } (4)
mov bx, ax { Sauve AX dans le registre BX } (4)
{ shl eax,16 } db 66h; shl ax,16 { Déplace les deux octets de poids faible de EAX dans les octets de poids fort } (4)
mov ax, bx { Relit AX du registre BX } (4)
mov cx,64000/4 { 16 000 double-mots à remplir } (5)
{rep stosd } db 66h; rep stosw { Ecrit ECX fois le double-mot EAX à l'adresse ES:EDI } (6)
end; (1)

Analyse :

  • (1) - Encore mieux, en turbo Pascal on peut déclarer une procédure qui n'utilise que l'assembleur, ce qui évite d'avoir à taper "begin" et "end;" inutiles.
  • (2) - Pour mettre un registre à zéro, la méthode la plus rapide est "xor reg,reg", car XOR est une fonction de base du processeur.
  • (3) - Turbo Pascal 7 ne connaissant pas les instructions 32 bits, on peut toujours les écrire en langage machine. Pour avoir le code machine d'une instruction, on peut compiler un programme en assembleur ne contenant que cette instruction en demandant au compileur de créer un listing (avec le paramètre "/Z" pour TASM 1.0 et 2.0), c'est à dire un fichier contenant le code assembleur décomposée, interprété et traduit en code machine. Sinon, il existe aussi des programmes effectuant la convertion, mais je n'en connais pas directement. Pour revenir à notre "xor edi, edi", l'équivalant en code machine est "66 33 FF", or "xor di,di" est équivaut à "33 FF", il suffit donc de rajouter le code "66h" en préfixe, préfixe de nombreuses instructions 32 bits en fait.
  • (4) - En 32 bits, l'instruction "STOSD" écrira EAX et non AL, il faut donc répéter "Coul" quatre fois dans EAX. On commence par l'écrire dans AL, puis le copier dans AH, et enfin un "truc" pour le copier dans EAX. Le truc est en fait de décaler les octets de EAX de 16 bits vers la gauche à l'aide de l'instruction SHL, puis recopier l'ancienne valeur de AX dans AX (qui est la partie basse de EAX pour souvenir !). Remarque : l'instruction "SHL" nécessite de compiler en mode 286/287, on peut rajouter "{$G+}" au début du programme pour ce fait.
  • (5) - STOSD écrit des double-mots (4 octets), or nous voulons remplir 64 000 octets. Il nous faudra donc écrire 64 000/4 fois EAX. Remarque : pour STOSW, on divisera par 2 tout simplement :-)
  • (6) - Une fois de plus, on peut étendre une instruction 16 bits à son équivalant en 32 bits en rajoutant le préfixe "66h". En réalité "REP STOSW" est "F3 AB" (F3 = "REP"), et "REP STOSD" est "F3 66 AB". Mais le processeur est tolérant, et le code est d'autant plus clair avec "db 66h; stosw", alors pourquoi s'en priver.

Copyright © 2012 haypo. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique Assembleur : Alcatîz -
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


ASM Mode Reel

Apprendre l'assembleur

Table des matières Plier Déplier

  • I. Introduction
  • II. PREMIÈRE PARTIE
    • II-A. L'ARITHMÉTIQUE SIGNÉE
    • II-B. LA MÉMOIRE VIVE
      • II-B-1. La segmentation de la mémoire
      • II-B-2. Structure d'un programme en mémoire
      • II-B-3. Les fichiers COM
      • II-B-4. Les fichiers EXE
      • II-B-5. Les cycles de lecture-écriture
    • II-C. LE MICROPROCESSEUR - LES REGISTRES
      • II-C-1. Généralités sur les registres
      • II-C-2. Les registres généraux
      • II-C-3. Les registres de segment
      • II-C-4. Les registres d'offset
      • II-C-5. Le registre des indicateurs
    • II-D. LES INTERRUPTIONS
      • II-D-1. Introduction
      • II-D-2. La table des vecteurs d'interruptions
      • II-D-3. Sauvegarde de l'état des registres lors de l'appel
  • III. DEUXIÈME PARTIE
    • III-A. PREMIER EXEMPLE : LES FICHIERS COM
    • III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE
  • IV. TROISIÈME PARTIE
    • IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE
      • IV-A-1. Les définitions de données
      • IV-A-2. L'adressage
        • IV-A-2-a. L'adressage immédiat
        • IV-A-2-b. L'adressage indexé et/ou basé
    • IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS
      • IV-B-1. Les sauts inconditionnels
        • IV-B-1-a. Les procédures
          • IV-B-1-a-i. appels de procédures
          • IV-B-1-a-ii. le passage des paramètres
        • IV-B-1-b. Les macros
        • IV-B-1-c. La directive EQU
        • IV-B-1-d. L'inclusion de fichiers
    • IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE
      • IV-C-1. L'instruction NOP (« No Operation »)
      • IV-C-2. L'instruction MOV (« Move »)
      • IV-C-3. L'instruction XCHG (« Exchange »)
      • IV-C-4. L'instruction JMP (« Jump »)
      • IV-C-5. L'opérateur CMP (« Compare »)
      • IV-C-6. Les instructions de saut conditionnel
        • IV-C-6-a. les sauts de comparaison
          • IV-C-6-a-i. les sauts de test sur les flags
          • IV-C-6-a-ii. le saut de test sur le registre CX
        • IV-C-6-b. Les instructions arithmétiques
          • IV-C-6-b-i. L'instruction INC (« Increment »)
          • IV-C-6-b-ii. L'instruction ADD (« Addition »)
          • IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)
          • IV-C-6-b-iv. L'instruction DEC (« Decrement »)
          • IV-C-6-b-v. L'instruction SUB (« Substract »)
          • IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)
          • IV-C-6-b-vii. L'instruction MUL (« Multiply »)
          • IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)
          • IV-C-6-b-ix. L'instruction DIV (« Divide »)
          • IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)
          • IV-C-6-b-xi. L'instruction NEG (« Negation »)
        • IV-C-6-c. Les instructions logiques
          • IV-C-6-c-i. L'instruction NOT (« Logical NOT »)
          • IV-C-6-c-ii. L'instruction OR (« Logical OR »)
          • IV-C-6-c-iii. L'instruction AND (« Logical AND »)
          • IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)
          • IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)
          • IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)
          • IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)
          • IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)
          • IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)
          • IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)
          • IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)
        • IV-C-6-d. Les instructions de manipulation des flags
          • IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)
          • IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)
          • IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)
          • IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)
          • IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)
          • IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)
          • IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)
          • IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)
          • IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)
        • IV-C-6-e. Les instructions de gestion de la pile
          • IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)
          • IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)
          • IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)
          • IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)
          • IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)
          • IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)
        • IV-C-6-f. Les instructions de gestion des chaînes d'octets
          • IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)
          • IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)
          • IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)
        • IV-C-6-g. Les instructions de gestion des chaînes de mots
          • IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)
          • IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)
          • IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)
          • IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)
          • IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)
        • IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots
          • IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)
          • IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)
          • IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)
          • IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)
          • IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)
        • IV-C-6-i. Les instructions d'appel
          • IV-C-6-i-i. L'instruction CALL (« Procedure Call »)
          • IV-C-6-i-ii. L'instruction RET (ou RETN)
          • IV-C-6-i-iii. L'instruction RETF
        • IV-C-6-j. Les instructions de boucle
          • IV-C-6-j-i. L'instruction LOOP
          • IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)
          • IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)
        • IV-C-6-k. Les instructions d'adressage
          • IV-C-6-k-i. L'instruction LEA (« Load effective address »)
          • IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)
          • IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)
        • IV-C-6-l. Les instructions de conversion arithmétique
          • IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
          • IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
          • IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
          • IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
          • IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)
          • IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)
          • IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
          • IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
          • IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)
          • IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)
          • IV-C-6-l-xi. L'instruction XLAT (« Translate »)
        • IV-C-6-m. Les instructions d'entrée-sortie
          • IV-C-6-m-i. L'instruction IN (« Input from port »)
          • IV-C-6-m-ii. L'instruction OUT (« Output to port »)
  • V. QUATRIÈME PARTIE
    • V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES
      • V-A-1. Ouverture d'un fichier
      • V-A-2. Lecture dans un fichier
      • V-A-3. Écriture dans un fichier
      • V-A-4. Existence d'un pointeur de fichier
      • V-A-5. Fermeture d'un fichier
      • V-A-6. Conclusion
    • V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS
      • V-B-1. La fonction 4eh
      • V-B-2. La fonction 4fh
      • V-B-3. Conclusion
      • V-B-4. EXEMPLE DE PROGRAMME
      • V-B-5. Entrée bufferisée au clavier
      • V-B-6. Le programme
  • VI. CONCLUSION

L'auteur

Benoît-M

L'article

Publié le 3 janvier 2003 - Mis à jour le 15 février 2013

I. Introduction

« Désormais, pour apprendre le français, il faudra SAVOIR le français ! » disait Coluche. De même, ce tutoriel s'adresse à tous ceux qui connaissent déjà les rudiments d'un langage évolué tel que le BASIC, le FORTRAN, le C ou le PASCAL et qui souhaitent apprendre l'assembleur. Une connaissance même sommaire d'un de ces langages suffit ! Aucune connaissance en programmation système n'est requise : le but de ce cours est justement d'en introduire les fondements.

Contrairement aux langages évolués, l'assembleur, ou « langage d'assemblage » est constitué d'instructions directement compréhensibles par le microprocesseur : c'est ce qu'on appelle un langage de bas niveau. Il est donc intimement lié au fonctionnement de la machine. C'est pourquoi il est relativement difficile à assimiler, en tout cas beaucoup plus que les langages de haut niveau.

Cela explique également pourquoi il existe au moins autant de langages d'assemblage que de modèles de microprocesseurs.

Avant d'apprendre l'assembleur INTEL 80x86, il est donc primordial de s'intéresser à quelques notions de base concernant par exemple la mémoire ou le microprocesseur. C'est là en effet que se trouvent les principales difficultés pour le débutant. Ne soyez pas rebuté par l'abstraction des concepts présentés dans les premiers paragraphes : il est normal que durant la lecture, beaucoup de choses ne soient pas claires dans votre esprit. Tout vous semblera beaucoup plus simple quand nous passerons à la pratique dans le langage assembleur.

Le microprocesseur peut fonctionner sous deux modes : le mode réel et le mode protégé. Le mode protégé permet d'accéder à 232 octets de mémoire vive, alors que le mode réel ne peut en adresser que 220 = 1 Mo. Nous ne traiterons dans ce cours que le mode réel. C'est celui qu'utilisent la plupart des programmes DOS.

Convention : toutes les adresses sont écrites en notation hexadécimale. Les autres nombres seront la plupart du temps représentés en base décimale. Dans le cas contraire, nous ajouterons la lettre ‘h' après les chiffres.

II. PREMIÈRE PARTIE

II-A. L'ARITHMÉTIQUE SIGNÉE

On appelle arithmétique non signée l'arithmétique dans laquelle tous les entiers sont positifs. En arithmétique signée au contraire, les nombres peuvent être soit positifs, soit négatifs. Un nombre signé n'est donc pas forcément négatif.

Les données informatiques se présentent sous la forme d'une succession de chiffres binaires, les bits. Nous supposerons que les systèmes de numération binaire et hexadécimal vous sont déjà familiers.

Il est en revanche fort possible que vous ne connaissiez pas la façon dont un ordinateur représente les nombres négatifs. Il existe deux conventions : la notation en signe et valeur absolue et la notation en complément à 2.

La première est extrêmement simple : le premier bit représente le signe, et les autres bits la valeur absolue du nombre. Le bit de signe vaut 1 si le nombre est strictement négatif, 0 sinon.

Par exemple, le nombre (-14) codé sur 8 bits s'écrit ainsi :

10001110

Cette convention n'est quasiment jamais utilisée en informatique. On lui préfère la représentation en complément à 2 dont le principe est le suivant :

  • les nombres positifs sont codés de la même façon qu'en convention « signe et valeur absolue » ;
  • les nombres négatifs sont obtenus en inversant tous les bits, puis en ajoutant 1.

Exemple : le nombre 14 codé sur 8 bits est représenté ainsi :

00001110

et (-14) ainsi :

  • inversion des bits : 11110001 ;
  • ajout d'une unité : 11110010 ;
  • résultat : 11110010.

Remarque : le résultat intermédiaire, 11110001, est appelé « complément à 1 ».

Vous allez immédiatement comprendre l'avantage de cette représentation. Faisons la somme de 14 et de (-14), de la même façon que s'il s'agissait d'entiers positifs :

00001110 + 11110010 = 100000000

Le résultat étant codé sur 8 bits, le 1 situé à gauche n'est pas pris en compte. On obtient donc 14 + (-14) = 0.

L'intérêt évident est que la différence de deux nombres peut se calculer avec le même algorithme que leur somme. Il suffit de transformer au préalable le nombre retranché en son opposé. Cette conversion est très simple et très rapide.

Au contraire, en représentation « signe et valeur absolue », on aurait eu besoin de nombreux algorithmes, car plusieurs cas se présentent.

Remarque : la représentation en complément à 2 revient en fait à écrire (-1) comme ceci :

11111111

(-2) comme cela :

11111110

(-3) comme cela :

11111101

etc.

Il y a ainsi une symétrie entre les nombres positifs et les nombres négatifs. Il en résulte que le bit le plus à gauche représente le signe, de la même façon qu'en notation « signe et valeur absolue ». Avant tout calcul, nous pouvons donc affirmer que le nombre 10010101 est négatif.

II-B. LA MÉMOIRE VIVE

II-B-1. La segmentation de la mémoire

Votre PC est conçu pour gérer 1 Mo (soit 220 octets) de mémoire vive en mode réel. Il faut donc 20 bits au minimum pour adresser toute la mémoire. Or en mode réel les bus d'adresses n'ont que 16 bits. Ils permettent donc d'adresser 216 = 65536 octets = 64 Ko, ce qui est insuffisant !

Afin de pallier ce manque, on utilise deux nombres pour adresser un octet quelconque de la RAM. Le premier est appelé adresse de segment, le second adresse d'offset. Ils seront stockés séparément.

La mémoire est découpée en segments de 64 Ko chacun. Un segment est donc en quelque sorte un gros bloc de mémoire auquel on peut accéder grâce à une adresse de segment qui désigne son numéro. Par exemple, le premier segment est le segment 0000 (en hexa), le deuxième est le 0001, le quarante-deuxième est le 0029, etc. Chaque numéro est codé sur 16 bits, c'est-à-dire 4 chiffres hexa.

Pour accéder à un octet particulier dans un segment, il suffit de compter le décalage de cet octet par rapport au début du segment. Ce décalage est obligatoirement inférieur ou égal à 65535 : il tient bien sur 16 bits lui aussi. On appelle ce décalage « offset ».

L'adresse d'un octet se note XXXX:YYYY où XXXX est l'adresse de segment et YYYY est l'offset (tous deux en notation hexadécimale, bien sûr).

Par exemple, le dix-septième octet de la RAM (le numéro 16) est situé à l'adresse 0000:0010. De même, l'octet 0000:0100 est l'octet numéro 256. Nous en arrivons à la petite subtilité qu'il convient de bien saisir, sous peine de ne rien comprendre à certains programmes en assembleur.

On pourrait penser que l'octet qui se trouve à l'adresse 0001:0000 est le numéro 65536. Il n'en est rien. C'est l'octet numéro 16.

Eh oui ! Les segments ne sont pas situés gentiment les uns à la suite des autres. Ils sont fort mal élevés et n'attendent pas que les segments qui les précèdent soient terminés avant de commencer ! Ils se marchent donc sur les pieds.

Autrement dit, le deuxième segment ne démarre pas à l'octet 65536 comme il devrait le faire s'il était bien sage, mais à l'octet 16 ! Le troisième démarre à l'octet 32 et ainsi de suite…

La notion de segment n'est pas tant physique que mathématique : elle sert à se repérer dans la RAM.

La conséquence immédiate de tout cela est qu'un octet n'a pas une adresse unique. Par exemple, l'octet numéro 66 peut être adressé par 0000:0042, mais aussi par 0001:0032, par 0002:0022, par 0003:0012 ou encore par 0004:0002. Toutes ces adresses sont équivalentes.

Voilà pour la subtilité. Si vous avez compris, vous devriez être capable de trouver facilement comment on calcule l'adresse effective d'un octet, c'est-à-dire sa position absolue dans la RAM.

Allez, un petit effort !

Voici la solution : si l'adresse de l'octet est A17C:022E, alors son adresse effective est A17C x 16 + 022E, soit A17C0 + 022E = A19EE. On a multiplié par 16, car le segment A17C débute à l'octet A17C x 16, puis on a simplement ajouté le décalage.

Au final, on a bien une adresse sur 20 bits puisqu'on obtient 5 chiffres hexa. Chaque petit bloc de 16 octets s'appelle un paragraphe.

Image non disponible

Il est souvent plus simple de considérer qu'un segment est un bloc de taille quelconque qui débute à une adresse effective multiple de 16 et qui permet, à l'aide de son adresse de segment et d'un offset, d'adresser le bloc entier (64 Ko au maximum). Cette définition est à notre avis celle qui a le plus de sens, et nous l'utiliserons tout au long de ce cours.

II-B-2. Structure d'un programme en mémoire

Lorsque l'utilisateur exécute un programme, celui-ci est d'abord chargé en mémoire par le système. Le DOS distingue deux modèles de programmes exécutables : les fichiers COM et les fichiers EXE.

La différence fondamentale est que les programmes COM ne peuvent pas utiliser plus d'un segment dans la mémoire. Leur taille est ainsi limitée à 64 Ko. Les programmes EXE ne sont quant à eux limités que par la mémoire disponible dans l'ordinateur.

II-B-3. Les fichiers COM

Lorsqu'il charge un fichier COM, le DOS lui alloue toute la mémoire disponible. Si celle-ci est insuffisante, il le signale à l'utilisateur par un message et annule toute la procédure d'exécution. Dans le cas contraire, il crée le PSP du programme au début du bloc de mémoire réservé, et copie le programme à charger à la suite.

Mais qu'est-ce donc qu'un PSP ?

Pour simplifier, le PSP (« Program Segment Prefix ») est une zone de 256 (= 100h) octets qui contient des informations diverses au sujet du programme. C'est dans le PSP que se trouve la ligne de commande tapée par l'utilisateur. Par exemple, le PSP d'un programme appelé MONPROG, exécuté avec la commande « MONPROG monfic.txt /S /H », contiendra la chaîne de caractères suivante : « monfic.txt /S /H ». Le programmeur a ainsi la possibilité d'accéder aux paramètres.

Voici à titre indicatif la structure simplifiée du PSP (ne vous souciez pas de ce que vous ne comprenez pas : pour l'instant, seules les deux dernières lignes nous intéressent vraiment) :

Offset | Description | Taille (octets)
00 h | Appel de l'int 20h | 2
02 h | Adresse du 1er segment qui se trouve au-delà du prog. | 2
04 h | Réservé | 1
05 h | Far call de l'int 21h (inutilisé) | 5
0A h | Vecteur de l'int 22h | 4
0E h | Vecteur de l'int 23h | 4
12 h | Vecteur de l'int 24h | 4
16 h | Réservé | 22
2C h | Segment du bloc d'environnement | 2
2E h | Réservé | 82
80 h | Nombre de caractères dans la ligne de commande sans compter le code ASCII 13 (retour chariot) | 1
81 h | Ligne de commande (à partir du caractère espace qui suit le nom du programme) + code ASCII 13 | 127

À présent que nous connaissons l'existence du PSP, il nous faut revenir sur un point important. Comme nous l'avons dit, un programme COM ne peut comporter qu'un seul segment, bien que le DOS lui réserve la totalité de la mémoire disponible. Ceci a deux conséquences. La première est que les adresses de segment sont inutiles dans le programme : les offsets seuls permettent d'adresser n'importe quel octet du segment. La seconde est que le PSP fait partie de ce segment, ce qui limite à 64 Ko - 256 octets la taille maximale d'un fichier COM. Cela implique également que le programme lui-même débute à l'offset 100h et non à l'offset 0h.

II-B-4. Les fichiers EXE

« Pour un fichier EXE, tout est toujours un peu plus compliqué. »
(Devise du programmeur débutant en assembleur)

Bien qu'il soit possible de n'utiliser qu'un seul segment à tout faire, la plupart des programmes EXE ont un segment réservé au code (c'est ainsi qu'on appelle les instructions du langage machine), un ou deux autres aux données, et un dernier à la pile.

La pile est une mémoire très spéciale qui sert comme son nom l'indique à empiler des données de 16 bits de façon temporaire. On peut ensuite retrouver ces données en les dépilant. Le « dépilage » se fait toujours dans l'ordre inverse de l'empilage.

Le PSP a lui aussi son propre segment. Le programme commence donc à l'offset 0h du segment de code et non à l'offset 100h.

Afin que le programme puisse être chargé et exécuté correctement, il faut que le système sache où commence et où s'arrête chacun de ces segments. À cet effet, les compilateurs créent un en-tête (ou « header ») au début de chaque fichier EXE. Ce header ne sera pas copié en mémoire. Son rôle est simplement d'indiquer au DOS (lors du chargement) la position relative de chaque segment dans le fichier.

II-B-5. Les cycles de lecture-écriture

Le code et les variables d'un programme se trouvent dans la mémoire vive. Pour accéder à une donnée en mémoire, le processeur place son adresse dans un bus d'adresse. Un cycle de lecture se met alors en place. Il consiste à retourner la donnée lue au processeur via le bus de données.

Pour l'écriture, l'adresse de la destination est transmise dans le bus d'adresse et la donnée à écrire est placée dans le bus de données.

Ouf !

Nous aurons l'occasion de revenir sur la mémoire vive dans les autres chapitres pour apporter quelques précisions. Vous devriez mieux comprendre toutes ces notions en lisant la suite, car jusqu'ici nous n'avons pas encore parlé de la façon dont l'ordinateur se sert des adresses pour accéder à des données ou pour exécuter du code. C'est là qu'intervient le microprocesseur…

II-C. LE MICROPROCESSEUR - LES REGISTRES

II-C-1. Généralités sur les registres

Le microprocesseur est le cœur de l'ordinateur. C'est lui qui est chargé de reconnaître les instructions et de les exécuter (ou de les faire exécuter). Chaque instruction se présente sous la forme d'une suite de bits qu'on représente en notation hexadécimale (exemple : B44C, ou 1011010001001100 en binaire). Une instruction se compose d'un code opérateur (le plus souvent appelé « opcode ») qui désigne l'action à effectuer (B4 dans notre exemple) et d'un « champ d'opérandes » sur lesquels porte l'opération (ici l'opérande est 4C).

Pour travailler, le microprocesseur utilise de petites zones où il peut stocker des données. Ces zones portent le nom de registres et sont très rapides d'accès puisqu'elles sont implantées dans le microprocesseur lui-même et non dans la mémoire vive.

Les registres des processeurs INTEL se classent en quatre catégories :

  • les registres généraux (16 bits) ;
  • les registres de segment (16 bits) ;
  • les registres d'offset (16 bits) ;
  • le registre des indicateurs (16 bits).

II-C-2. Les registres généraux

Ils ne sont pas réservés à un usage très précis, aussi les utilise-t-on pour manipuler des données diverses. Ce sont en quelque sorte des registres à tout faire. Chacun de ces quatre registres peut servir pour la plupart des opérations, mais ils ont tous une fonction principale qui les caractérise.

Nom | Fonction privilégiée
AX | Accumulateur
BX | Base
CX | Compteur
DX | Données

Le registre AX sert souvent de registre d'entrée-sortie : on lui donne des paramètres avant d'appeler une fonction ou une procédure. Il est également utilisé pour de nombreuses opérations arithmétiques, telles que la multiplication ou la division de nombres entiers. Il est appelé « accumulateur ».

Exemples d'utilisation :

  • l'instruction “MOV AX, 1982” place l'entier 1982 dans AX ;
  • “ADD AX, 1983” ajoute à AX le nombre 1983 et place le résultat dans AX.

Le registre BX peut servir de base. Nous verrons plus tard ce que ce terme signifie.

Le registre CX est utilisé comme compteur dans les boucles. Par exemple, pour répéter 15 fois une instruction en assembleur, on peut mettre la valeur 15 dans CX, écrire l'instruction précédée d'une étiquette qui représente son adresse en mémoire, puis faire un LOOP à cette adresse. Lorsqu'il reconnaît l'instruction LOOP, le processeur « sait » que le nombre d'itérations à exécuter se trouve dans CX. Il se contente alors de décrémenter CX, de vérifier que CX est différent de 0 puis de faire un saut (« jump ») à l'étiquette mentionnée. Si CX vaut 0, le processeur ne fait pas de saut et passe à l'instruction suivante.

Exemple :

Image non disponible

Le registre DX contient souvent l'adresse d'un tampon de données lorsqu'on appelle une fonction du DOS. Par exemple, pour écrire une chaîne de caractères à l'écran, il faut placer l'offset de cette chaîne dans DX avant d'appeler la fonction appropriée.

Chacun de ces quatre registres comporte 16 bits. On peut donc y stocker des nombres allant de 0 à 65535 en arithmétique non signée, et des nombres allant de -32768 à 32767 en arithmétique signée. Les 8 bits de poids fort d'un registre sont appelés « partie haute » et les 8 autres « partie basse ». Chaque registre est en fait constitué de deux « sous-registres » de 8 bits. La partie haute de AX s'appelle AH, sa partie basse AL. Il en va de même pour les trois autres.

En arithmétique non signée, il en découle la formule suivante :

AX = AH x 256 + AL

ainsi que leurs homologues :

BX = BH x 256 + BL

CX = CH x 256 + CL

DX = DH x 256 + DL

Il va de soi qu'en modifiant AL ou AH, on modifie également AX.

Pour stocker un nombre de 32 bits, on peut utiliser des paires de registres. Par exemple, DX:AX signifie DX x 65535 + AX en arithmétique non signée.

Cette notation (DX:AX) n'est pas reconnue par l'assembleur (ni par la machine). Ne confondez pas cela avec les adresses de segment et d'offset.

Sur un PC relativement récent, AX, BX, CX, DX ne sont en fait que les parties basses de registres de 32 bits nommés EAX, EBX, ECX, EDX (« E » pour « Extended »). On a donc un moyen plus pratique de stocker les grands nombres. En fait, chaque registre (pas seulement les registres généraux) peut contenir 32 bits.

II-C-3. Les registres de segment

Ils sont au nombre de quatre :

Nom | Nom complet | Traduction
CS | Code segment | Segment de code
DS | Data segment | Segment de données
ES | Extra segment | Extra segment
SS | Stack segment | Segment de pile

Contrairement aux registres généraux, ces registres ne peuvent servir pour les opérations courantes : ils ont un rôle très précis. On ne peut d'ailleurs pas les utiliser aussi facilement que AX ou BX, et une petite modification de l'un d'eux peut suffire à « planter » le système. Eh oui ! L'assembleur, ce n'est pas Turbo Pascal ! Il n'y a aucune barrière de protection, si bien qu'une petite erreur peut planter le DOS. Mais rassurez-vous : tout se répare très bien en redémarrant l'ordinateur…

Dans le registre CS est stockée l'adresse de segment de la prochaine instruction à exécuter. La raison pour laquelle il ne faut surtout pas changer sa valeur directement est évidente. De toute façon, vous ne le pouvez pas. Le seul moyen viable de le faire est d'utiliser des instructions telles que des sauts (JMP) ou des appels (CALL) vers un autre segment. CS sera alors automatiquement actualisé par le processeur en fonction de l'adresse d'arrivée.

Le registre DS est quant à lui destiné à contenir l'adresse du segment des données du programme en cours. On peut le faire varier à condition de savoir exactement pourquoi on le fait. Par exemple, on peut avoir deux segments de données dans son programme et vouloir accéder au deuxième. Il faudra alors faire pointer DS vers ce segment.

ES est un registre qui sert à adresser le segment de son choix. On peut le changer aux mêmes conditions que DS. Par exemple, si on veut copier des données d'un segment vers un autre, on pourra faire pointer DS vers le premier et ES vers le second.

Le registre SS adresse le segment de pile. Il est rare qu'on doive y toucher car le programme n'a qu'une seule pile.

Intéressons-nous à présent aux valeurs que le DOS donne à ces registres lors du chargement en mémoire d'un fichier exécutable ! Elles diffèrent selon que le fichier est un programme COM ou EXE. Pour écrire un programme en assembleur, il est nécessaire de connaître ce tableau par cœur :

Registre | Valeur avant l'exécution
 | Fichier COM | Fichier EXE
CS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de code
DS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
ES | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
SS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de pile

Le schéma suivant montre mieux la situation :

Image non disponible

Dans un fichier EXE, le header indique au DOS les adresses initiales de chaque segment par rapport au début du programme (puisque le compilateur n'a aucun moyen de connaître l'adresse à laquelle le programme sera chargé). Lors du chargement, le DOS ajoutera à ces valeurs l'adresse d'implantation pour obtenir ainsi les véritables adresses de segment.
Dans le cas d'un fichier COM, tout est plus simple. Le programme ne comporte qu'un seul segment, donc il suffit tout bêtement au DOS de charger CS, DS, ES et SS avec l'adresse d'implantation.

Remarque : Pourquoi DS et ES pointent-ils vers le PSP dans le cas d'un fichier EXE ? Première raison : pour que le programmeur puisse accéder au PSP ! Deuxième raison : parce qu'un programme EXE peut comporter un nombre quelconque de segments de données. C'est donc au programmeur d'initialiser ces registres, s'il veut accéder à ses données.

II-C-4. Les registres d'offset

Les voici :

Nom | Nom complet | Traduction
IP | Instruction pointer | Pointeur d'instruction
SP | Stack pointer | Pointeur de pile
SI | Source index | Index de source
DI | Destination index | Index de destination
BP | Base pointer | Pointeur de base

Le registre IP désigne l'offset de la prochaine instruction à exécuter, par rapport au segment adressé par CS. La combinaison de ces deux registres (i.e. CS:IP) suffit donc à connaître l'adresse absolue de cette instruction. Le processeur peut alors aller la chercher en mémoire et l'exécuter. De plus, il actualise IP en l'incrémentant de la taille de l'instruction en octets. Tout comme CS, il est impossible de modifier IP directement.

Le registre SP désigne le sommet de la pile. Il faut bien comprendre le fonctionnement de la pile, aussi allons-nous insister sur ce point. La pile ne peut stocker que des mots. On appelle un mot (« word » en anglais) un nombre codé sur deux octets (soit 16 bits). Prenons un exemple simple : un programme COM. Le segment de pile (adressé par SS) et le segment de code ne font qu'un. Avant l'exécution, SP vaut FFFE. C'est l'offset de la dernière donnée de 16 bits empilée, par rapport à SS bien sûr. Pourquoi FFFE ? Tout simplement parce que la pile se remplit à l'envers, c'est-à-dire en partant de la fin du segment et en remontant vers le début. Le premier mot empilé se trouve à l'offset FFFE. Il tient sur deux octets : l'octet FFFE et l'octet FFFF. Mais comment se fait-il qu'un mot soit déjà empilé avant le début du programme ? Ce mot est un zéro que le DOS place sur la pile avant l'exécution de tout programme COM. Nous en verrons la raison plus tard.

À présent, que se passe-t-il si à un instant quelconque une instruction ordonne au processeur d'empiler un mot ? Eh bien le stack pointer sera décrémenté de 2 et le mot sera copié à l'endroit pointé par SP. Rappelez-vous que la pile se remplit à l'envers ! C'est pour cette raison que SP est décrémenté à chaque empilage et non pas incrémenté.

Un petit exemple pour rendre les choses plus concrètes :

PUSH AX

L'effet de cette instruction est d'empiler le mot contenu dans le registre AX. Autrement dit, SP est automatiquement décrémenté de 2, puis AX est copié à l'adresse SS:SP.

Lors du dépilage, le mot situé au sommet de la pile, c'est-à-dire le mot adressé par SS:SP, est transféré dans un registre quelconque choisi par le programmeur, après quoi le stack pointer est incrémenté de 2.

Exemple :

POP BX

Cette fois, on retire le dernier mot empilé pour le placer dans le registre BX. Évidemment, SP sera incrémenté de 2 aussitôt après.

La pile est extrêmement utile lorsqu'il s'agit de stocker provisoirement le contenu d'un registre qui doit être modifié.

Exemple :

Image non disponible

Il est important de comprendre qu'on ne peut dépiler que le mot qui se trouve au sommet de la pile. Le premier mot empilé est le dernier qui sera dépilé. La pile doit être manipulée avec une extrême précaution. Un dépilage injustifié fait planter la machine presque systématiquement.

Les trois derniers registres sont beaucoup moins liés au fonctionnement interne du processeur. Ils sont mis à la disposition du programmeur qui peut les modifier à sa guise et les utiliser comme des registres généraux. Comme ces derniers cependant, ils ont une fonction qui leur est propre : servir d'index (SI et DI) ou de base (BP). Nous allons expliciter ces deux termes.

Dans la mémoire, les octets se suivent et forment parfois des chaînes de caractères. Pour utiliser une chaîne, le programmeur doit pouvoir accéder facilement à tous ses octets, l'un après l'autre. Or pour effectuer une opération quelconque sur un octet, il faut connaître son adresse. Cette adresse doit en général être une constante évaluable par le compilateur. Pourquoi une constante ? Parce que l'adresse est un opérande comme les autres, elle se trouve immédiatement après l'opcode et doit donc avoir une valeur numérique fixe !

Prenons un exemple :

MOV AH, [MonOctet]

Pas de panique ! Cette instruction en assembleur signifie « Mettre dans AH la valeur de l'octet adressé par le label MonOctet ! ». À la compilation, « MonOctet » sera remplacé par la valeur numérique qu'il représente et on obtiendra alors une instruction en langage machine telle que :

8A260601

8A26 est l'opcode (hexa) de l'instruction “MOV AH, [constante quelconque]”, et 0601 est l'offset de « MonOctet ».

Il serait pourtant fastidieux, dans le cas d'une chaîne de 112 caractères, de traiter les octets avec 112 instructions dans lesquelles seule l'adresse changerait. Il faudrait pouvoir faire une boucle sur l'adresse, mais alors celle-ci ne serait plus une constante, d'où le problème.

Pour les constructeurs du microprocesseur, la seule solution était de créer de nouveaux opcodes pour chaque opération portant sur un octet en mémoire. Ces opcodes spéciaux feraient la même action que ceux dont ils seraient dérivés, mais l'adresse passée en paramètre serait alors considérée comme un décalage par rapport à un registre spécial. Il suffirait donc de faire varier ce registre, et le processeur y ajouterait automatiquement la valeur de l'opérande pour obtenir l'adresse réelle ! C'est à cela que servent SI, DI et BP.

Par exemple :

MOV AH, [MonOctet + DI]

sera codé :

8AA50601

8AA5 est l'opcode pour l'instruction “MOV AH, [constante quelconque + DI]”.

Remarque : les registres SI et BP auraient tout aussi bien pu être employés, mais pas les registres généraux, SAUF BX. En effet, BX peut jouer exactement le même rôle que BP. N'oubliez pas que BX est appelé registre de « base », et que BP signifie « Base Pointer. » Nous verrons la différence entre une base et un index lorsque nous commencerons l'assembleur.

II-C-5. Le registre des indicateurs

Un programme doit pouvoir faire des choix en fonction des données dont il dispose. Pour cela, il lui faut par exemple comparer des nombres, examiner leur signe, découvrir si une erreur a été constatée, etc.

Il existe à cet effet de petits indicateurs, les flags qui sont des bits spéciaux ayant une signification très précise. De manière générale, les flags fournissent des informations sur les résultats des opérations précédentes.

Ils sont tous regroupés dans un registre : le registre des indicateurs. Comprenez bien que chaque bit a un rôle qui lui est propre et que la valeur globale du registre ne signifie rien. Le programmeur peut lire chacun de ces flags et parfois modifier leur valeur directement.

En mode réel, certains flags ne sont pas accessibles. Nous n'en parlerons pas. Nous ne commenterons que les flags couramment utilisés.

Nous verrons quelle utilisation on peut faire de ces indicateurs dans la troisième partie de ce tutoriel.

  • CF (« Carry Flag ») est l'indicateur de retenue. Il est positionné à 1 si et seulement si l'opération précédemment effectuée a produit une retenue. De nombreuses fonctions du DOS l'utilisent comme indicateur d'erreur : CF passe alors à 1 en cas de problème.
  • PF (« Parity Flag ») renseigne sur la parité du résultat. Il vaut 1 si ce dernier est pair.
  • AF (« Auxiliary Carry Flag ») est peu utilisé.
  • ZF (« Zero Flag ») passe à 1 si le résultat d'une opération est égal à zéro.
  • SF (« Sign Flag ») passe à 1 si le résultat d'une opération sur des nombres signés est négatif.
  • TF (« Trap Flag ») est utilisé pour le « débogage » d'un programme. S'il vaut 1, une routine spéciale du débogueur est appelée après l'exécution de chaque instruction par le processeur.
  • IF (« Interrupt Flag ») sert à empêcher les appels d'interruptions lorsqu'il est positionné à 1. Cependant, toutes les interruptions ne sont pas « masquables ».
  • DF (« Direction Flag ») est utilisé pour les opérations sur les chaînes de caractères. S'il vaut 1, celles-ci seront parcourues dans le sens des adresses décroissantes, sinon les adresses seront croissantes.
  • OF (« Overflow Flag ») indique qu'un débordement s'est produit, c'est-à-dire que la capacité de stockage a été dépassée. Il est utile en arithmétique signée. Avec des nombres non signés, il faut utiliser ZF et SF.

Remarque : Les notations CF, PF, AF, etc. ne sont pas reconnues par l'assembleur. Pour utiliser les flags, il existe des instructions spécifiques que nous décrirons plus tard.

II-D. LES INTERRUPTIONS

II-D-1. Introduction

Le microprocesseur ne peut exécuter qu'une seule instruction à la fois. Pour connaître son adresse, il utilise le couple de registres CS:IP dont la valeur est incrémentée automatiquement. Par conséquent, le code du programme courant est exécuté de manière linéaire.

Imaginons cependant qu'un événement extérieur demande l'attention de l'ordinateur, par exemple la pression d'une touche du clavier. La machine doit pouvoir réagir immédiatement, sans attendre que le programme en cours d'exécution se termine. Pour cela, elle interrompt ce dernier pendant un bref instant, le temps de traiter l'événement survenu puis rend le contrôle au programme interrompu.

Une interruption n'est rien d'autre que l'appel d'une routine spéciale présente en mémoire appelée ISR (« Interrupt Service Routine »).

Les interruptions se divisent en trois catégories :

  • les interruptions électroniques, par exemple : le clavier ;
  • les interruptions du BIOS, par exemple : l'accès aux disques ;
  • les interruptions du DOS, par exemple : l'accès aux systèmes de fichiers.

Comment sont-elles déclenchées ?

Une interruption peut être déclenchée par votre matériel. C'est ce qui arrive lorsque vous appuyez sur une touche du clavier. Aucun logiciel n'intervient et le contrôle est passé directement à la routine qui gère le clavier : ce sont les interruptions matérielles.

Les interruptions logicielles sont quant à elles appelées par des instructions en langage machine au sein d'un programme. Leur importance est capitale. Rappelez-vous que contrairement au PASCAL ou au C, l'assembleur ne dispose pas de fonction préprogrammée. Chaque instruction doit être directement traduisible en langage machine.

Mais alors, comment fait-on pour écrire une chaîne de caractères à l'écran ? Ou bien pour lire un caractère entré au clavier ?

Eh bien on le fait de la même façon que le DOS lui-même ! On déclenche les interruptions appropriées à l'aide de l'instruction « INT » du langage machine. C'est donc une routine du DOS (ou parfois du BIOS) qui fera tout le travail. Les paramètres (ou leurs adresses) sont passés dans les registres.

Voici un petit exemple en assembleur qui écrit la lettre ‘A' à l'écran :

Image non disponible

Examinons-le ligne par ligne :

  • l'instruction « MOV DL, ‘A' » demande au processeur de mettre dans le registre DL le code ASCII de la lettre ‘A', c'est-à-dire 65, ou 41h ;
  • « MOV AH, 02 » : mettre le nombre 2 dans AH ;
  • enfin, la dernière instruction appelle l'interruption numéro 21h. Il existe 256 interruptions. Toutes sont notées en base hexadécimale.

Explications :

Comme vous aurez très vite l'occasion de vous en rendre compte, l'interruption 21h est l'interruption du DOS par excellence. Elle permet d'appeler de nombreuses fonctions très diverses. Pour cela, il suffit de mentionner leur numéro dans le registre AH.

Il est très difficile de mémoriser le rôle de chaque interruption, et a fortiori de chaque fonction ou sous-fonction, d'autant plus qu'elles sont désignées par des numéros hexadécimaux et qu'elles attendent des paramètres dans des registres précis. C'est pourquoi tout programmeur se doit d'avoir à sa disposition une liste des interruptions pour travailler. Celle de Ralph Brown est très connue, et vous la trouverez sur l'Internet.

Revenons à notre exemple. La fonction numéro 2 de l'interruption 21h sert à écrire un caractère à l'écran. Il faut pour cela écrire le code ASCII du caractère dans le registre DL et bien sûr placer le nombre 2 dans AH.

Une fois que AH et DL ont été ajustés, l'interruption 21h peut être appelée à l'aide de l'instruction INT.

D'autres interruptions ne remplissent qu'une seule tâche. Vous n'avez donc pas besoin de mettre un numéro de fonction dans AH. L'appel de l'interruption suffit.

II-D-2. La table des vecteurs d'interruptions

À chaque appel d'interruption, l'ordinateur doit pouvoir trouver l'adresse de l'ISR correspondante. Pour cela, il dispose de la table des vecteurs d'interruptions (TVI, ou IVT : « Interrupt Vector Table »). Cette table est implantée à l'adresse 0000:0000 c'est-à-dire au début de la RAM.

La table est organisée comme suit :

Adresse | Taille (octets) | Valeur
0000:0000 | 2 | Adresse d'offset de l'interruption numéro 0
0000:0002 | 2 | Adresse de segment de l'interruption numéro 0
0000:0004 | 2 | Adresse d'offset de l'interruption numéro 1
0000:0006 | 2 | Adresse de segment de l'interruption numéro 1
0000:0008 | 2 | Adresse d'offset de l'interruption numéro 2
0000:000A | 2 | Adresse de segment de l'interruption numéro 2
0000:000C | 2 | Adresse d'offset de l'interruption numéro 3
0000:000E | 2 | Adresse de segment de l'interruption numéro 3
etc. etc. etc.
0000:03FC | 2 | Adresse d'offset de l'interruption numéro 255
0000:03FE | 2 | Adresse de segment de l'interruption numéro 255

La raison pour laquelle les offsets précèdent les adresses de segment vient du codage en « little endian » utilisé par INTEL. Les processeurs de cette marque (contrairement à la plupart des autres, qui travaillent en « big endian ») ont une représentation des données en mémoire aussi curieuse qu'insupportable pour le programmeur : ils placent le poids fort après le poids faible. Ainsi, le mot 4A28h sera codé 284Ah. Puisque l'adresse de l'ISR tient sur 32 bits (16 + 16), elle est représentée comme un double-mot (« dword »), donc l'adresse d'offset, qui est le mot de poids faible, se trouve au début. Eh oui, c'est pénible, mais il faudra s'y faire !

Remarque : pour calculer l'adresse de l'entrée dans la TVI correspondant à l'interruption numéro X, il suffit de multiplier X par 4.

Exemple : l'adresse de l'ISR numéro 21h est stockée à 0000:0084.

II-D-3. Sauvegarde de l'état des registres lors de l'appel

Un appel d'interruption obéit à certaines règles, car il est indispensable, une fois l'ISR exécutée, que le programme interrompu retrouve les registres dans le même état qu'ils étaient auparavant. Tout doit se passer comme si l'interruption n'avait jamais eu lieu. C'est pourquoi avant de faire un saut à l'ISR pointée par l'entrée correspondante dans la TVI, l'ordinateur sauvegarde tous les registres sur la pile courante. Il restaurera leur contenu avant de rendre le contrôle. Cette procédure est automatique. Il n'incombe pas au programmeur de prendre toutes ces précautions.

III. DEUXIÈME PARTIE

Cette partie a pour but de vous présenter l'architecture du langage assembleur à travers de courts exemples. Ne vous inquiétez pas si vous ne comprenez pas parfaitement certaines instructions : toutes seront détaillées dans la troisième partie. L'essentiel est que vous compreniez grossièrement ce qu'on fait et pourquoi on le fait . Les explications qui accompagnent ces exemples devraient y suffire. Vous pourrez ensuite apprendre le langage en lui-même en consultant la partie suivante.

Remarques pratiques préliminaires

  • Les mots qui sont imprimés en italiques sont ceux qui ne font pas partie du langage en lui-même. Ils sont choisis par le programmeur.
  • Tous les programmes présentés ci-dessous suivent la syntaxe du compilateur TASM. Il existe de très légères différences d'un compilateur à l'autre.
  • Le code peut être écrit en majuscules ou en minuscules. Chaque mot est séparé des autres par des espaces ou des tabulations.
  • Les commentaires sont précédés du signe ‘;'.
  • Les nombres doivent toujours commencer par un chiffre (entre 0 et 9), même les nombres hexadécimaux. Il faut donc écrire 0F2Ah et non pas F2Ah. Par contre, l'écriture 5CFh est correcte.
  • Les apostrophes et les guillemets sont équivalents. Il est cependant plus commode de réserver l'usage des guillemets aux chaînes de plusieurs caractères et celui des apostrophes aux caractères isolés.
  • Vous pouvez taper vos programmes avec l'éditeur de texte du DOS (EDIT.COM). Donnez-leur l'extension ‘.asm'.
  • La compilation se fait en tapant “TASM /m9 MONPROG” si votre source s'appelle ‘MONPROG.ASM'.

    Le paramètre ‘/m9', facultatif, indique au compilateur qu'il devra effectuer 9 passes, c'est-à-dire qu'il examinera 9 fois le code source. Comme chaque examen se fait de manière linéaire, le compilateur trouve parfois des instructions qui font référence à des labels placés plus loin dans le programme. Il manque donc d'informations, mais il continue son examen jusqu'à la fin. Quand il a terminé, il recommence tout depuis le début pour résoudre les problèmes qui s'étaient posés.

    La compilation crée un fichier objet (‘.obj'). Pour obtenir un fichier EXE, tapez ”TLINK MONPROG”. Pour un fichier COM, tapez “TLINK /tdc MONPROG”.

    Après l'édition des liens (le « linkage »), vous pouvez supprimer les fichiers MONPROG.obj' et ‘MONPROG.map'.

  • Il est possible de créer un fichier BAT qui s'occupe de toutes ces étapes. Ouvrez l'éditeur EDIT du DOS et tapez un programme tel que celui-ci :

    Enregistrez-le et nommez-le MAKE.BAT. Vous pouvez compiler et linker en tapant “MAKE MONPROG”. Pour compiler des fichiers COM, ajoutez le paramètre '/tdc' à la commande TLINK.

    Sélectionnez
    @ECHO OFF
TASM /m9 %1.asm
IF NOT EXIST %1.obj GOTO FIN
TLINK %1.obj REM : ajouter ici /tdc pour obtenir un fichier COM
ERASE %1.map
ERASE %1.obj
:FIN
  • Pour pouvoir lancer TASM et TLINK quel que soit le dossier courant, changez la variable PATH ainsi :

    PATH = %path%;c:\MonChemin\DossierTASM

    Vous pouvez inclure cette ligne à la fin de votre fichier AUTOEXEC.BAT afin qu'elle soit exécutée à chaque démarrage.

  • Vous pouvez déboguer vos programmes (par exemple les exécuter instruction par instruction en observant les changements induits dans la RAM et dans les registres…) avec le logiciel Turbo Debugger 16/32 bits qui est très pratique et très performant. À défaut, vous pouvez vous rabattre sur l'archaïque DEBUG.COM (il se trouve dans votre dossier de commandes DOS). Mais alors bon courage !

Le seul travail du compilateur (et du linkeur) est de convertir chacune de vos instructions en son équivalent en langage machine. Le programme compilé présentera donc exactement la même structure et la même linéarité que votre code source .

III-A. PREMIER EXEMPLE : LES FICHIERS COM

Voici un petit programme COM qui écrit le message « Bonjour, monde ! » à l'écran.

Image non disponible

Ce code source commence par des directives. Une directive est une information que le programmeur fournit au compilateur. Elle n'est pas transformée en une instruction en langage machine. Elle n'ajoute donc aucun octet au programme compilé.

La directive “.386“ indique au compilateur que le programme est destiné à tourner sur des processeurs INTEL de modèle 386 (ou supérieur). Cela nous autorise à utiliser certaines instructions qui ne sont pas disponibles sur les modèles antérieurs, comme PUSHA ou POPA. Dans cet exemple, cette directive aurait très bien pu être omise.

La ligne

code segment use16

sert à déclarer un segment que l'on appelle “code”. On aurait tout aussi bien pu le nommer “marteau” ou “voiture”. Ce sera le segment de notre programme. N'oubliez pas qu'un fichier COM ne peut comporter qu'un seul segment. Cette ligne ne sera pas compilée : elle ne sert qu'à indiquer au compilateur le début d'un segment.

Le mot “use16” indique que les adresses de segment et d'offset sont codées sur 16 bits et non sur 8 bits. Vous devez systématiquement l'écrire.

La directive

assume cs:code, ds:code, ss:code

informe le compilateur que tout au long du programme, CS, DS et SS pointeront de façon privilégiée vers le segment “code”, ce qui est évident d'ailleurs puisque c'est le seul segment… Vous comprendrez le sens exact de “assume” dans la troisième partie du cours.

Enfin, les mots

org 100h

signifient qu'il faudra ajouter 100h (soit 256) à tous les offsets. Pourquoi ? Souvenez-vous de la structure d'un programme COM en mémoire. Si vous n'écrivez pas cette ligne, TASM considérera que le programme débute à l'offset 0000. Or, lors de l'exécution, le DOS le chargera après le PSP, c'est-à-dire à l'adresse 100h. C'est pourquoi il est nécessaire de recalculer les offsets : l'offset 0000 deviendra 0100. Cette directive est en quelque sorte le trait caractéristique des fichiers COM.

Nous en arrivons au programme proprement dit. Il commence par un label :

debut :

Lui non plus n'est pas compilé. Il ne sert qu'à représenter l'adresse de l'instruction qui le suit, c'est-à-dire :

mov ah, 09h

Cette ligne demande au processeur de charger la valeur 9 dans le registre AH. C'est le numéro de la fonction de l'interruption 21h qui écrit une chaîne de caractères à l'écran. L'offset de cette chaîne est attendu dans DX. D'où la ligne suivante :

mov dx, offset message

Le mot-clé “offset” sert à extraire l'offset du label “message” qui représente quant à lui l'adresse du message à écrire (il contient donc une adresse de segment ET un offset). L'adresse de segment de la chaîne doit être transmise dans DS. Mais il est inutile de changer ce registre, car il pointe déjà vers notre segment.

Il nous reste à appeler l'interruption 21h :

int 21h

et à rendre la main au DOS :

ret

Lorsque nous aborderons les procédures, vous comprendrez mieux le sens de ce mot et ce qu'il fait exactement. Pour l'instant, sachez simplement que seul un fichier COM peut se terminer avec cette instruction.

Nous arrivons à la ligne :

message db “Bonjour, monde !”, ‘$'

Il s'agit d'une définition de données. Le mot “db” (« define byte ») signifie que le compilateur devra écrire les octets qui suivent tels qu'ils sont dans notre code source. Il va donc écrire le code ASCII du ‘B', puis celui du ‘o', etc. Il terminera en écrivant le code ASCII du signe ‘$'. C'est ainsi que la fonction 9 de l'interruption 21h reconnaît la fin de la chaîne à écrire. Si vous oubliez ce signe, elle écrira tous les octets de la RAM jusqu'à ce qu'elle tombe par hasard sur lui.

Le mot “message” placé en début de ligne est un label de données. Il représente l'adresse du code ASCII du ‘B'. Remarquez qu'il n'y a pas de caractère ‘:' après un label de données.

La ligne

code ends

indique la fin du segment « code », et enfin

end debut

informe le compilateur que le fichier est fini, tout comme le “END.” du PASCAL. Le nom du label « debut » est mentionné : ce sera le point d'entrée de notre programme. C'est vers lui que pointera CS:IP avant l'exécution.

Remarque : vous n'êtes pas tenu de rendre aux registres la valeur qu'ils avaient au début de votre programme. De toute façon, avant de charger un programme, le DOS sauvegarde le contenu de tous les registres puis met le contenu des registres généraux (ainsi que SI, DI et BP) à zéro. Il les restaurera quand vous lui rendrez la main.

III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE

Le programme suivant est un programme EXE qui fait exactement la même chose que l'exemple précédent.

Image non disponible

Examinons-le !

Tout d'abord, la directive

org 100h

a disparu.

En effet, lors de l'exécution, CS pointera vers notre segment de code et non pas vers le PSP. Il est donc inutile de décaler les offsets de 256.

Remarquons que notre programme dispose de deux segments supplémentaires :

  • le segment « data » est destiné à contenir les données, c'est-à-dire les variables ;
  • le segment « pile » sera notre segment de pile.

Notre directive « assume » devient donc :

assume cs:code, ds:data, ss:pile

Ainsi, le compilateur est informé que DS pointera vers le segment « data » et SS vers « pile ».

Les deux lignes suivantes,

mov ax, data

mov ds, ax

servent à initialiser le registre DS. Celui-ci pointe vers le PSP au début du programme, mais nous voulons le faire pointer vers notre segment de données appelé « data ». Cela est nécessaire puisque la fonction 9 de l'interruption 21h attend l'adresse de la chaîne dans le couple DS:DX et que notre message se trouve dans le segment de données.

La première instruction charge l'adresse du segment « data » dans AX. La seconde transfère cette valeur de AX dans DS.

Mais pourquoi diable utiliser AX comme intermédiaire ? Après tout, on pourrait écrire :

mov ds, data

Eh bien non ! Pour la simple raison que DS est un registre de segment et qu'en tant que tel on ne peut pas lui charger de valeur immédiate.

On appelle « valeur immédiate » toute constante tapée directement dans l'instruction elle-même.

Exemples de chargement de valeurs immédiates :

mov ax, 135 ;charge 135 dans AX

mov bx, offset message ;charge l'offset de message dans BX

mov bx, offset fin - offset debut ;charge le nombre d'octets entre fin et debut dans BX

mov es, 10 ;instruction illicite, car ES est un registre de segment !

Remarque : une autre possibilité aurait été d'écrire : “PUSH AX” (empiler AX) puis “POP DS” (dépiler le dernier nombre empilé et le placer dans DS).

Les trois lignes suivantes :

mov ah, 09h

mov dx, offset message

int 21h

n'ont pas changé.

Il nous faut également terminer le programme par un appel de la fonction 4ch de l'interruption 21h. C'est ainsi que se terminent les programmes EXE.

mov ah, 4ch

int 21h

Remarque : on aurait également pu écrire :

mov ax, 4c00h

int 21h

La seule différence est que AL est mis à zéro, ce qui indique au programme à qui on rend le contrôle (ici le DOS) que notre programme s'est terminé normalement. Les fichiers COM peuvent également utiliser la fonction 4ch.

Le segment de code se termine :

code ends

et le segment de données commence à sa suite :

data segment use16

message db “Bonjour, monde !”, ‘$'

data ends

Il nous reste à écrire le segment de pile. Dans ce programme, il n'était pas absolument indispensable de le séparer du segment de code. Mais c'est une bonne habitude de le faire.

pile segment stack

remplissage db 256 DUP (?)

pile ends

Le mot-clé « stack » indique que ce segment est le segment de pile.

Les mots « db 256 DUP (?) » déclarent 256 octets non initialisés. C'est la « matière » de notre pile.

Sachez que tout appel d'interruption se traduit par l'empilage des flags et de CS:IP. Il est donc indispensable d'avoir une pile, même si celle-ci peut éventuellement partager le même segment que le code, comme dans un fichier COM. Mais dans un programme EXE, il vaut mieux réserver un segment à la pile. Les 256 octets que nous déclarons ici indiquent seulement que la pile contient 256 octets. Ainsi, au début de l'exécution, SS:SP pointera vers la fin de ces octets. Nous verrons ce que signifie le point d'interrogation dans la troisième partie.

La fin du fichier et le point d'entrée sont signalés par :

end debut

Et voilà ! Vous avez découvert l'allure d'un programme en assembleur. Nous pouvons donc à présent nous pencher sur l'étude du langage.

IV. TROISIÈME PARTIE

IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE

IV-A-1. Les définitions de données

Les mots “db” (« define byte »), “dw” (« define word »), ”dd” (« define double word ») permettent de déclarer et d'initialiser une variable. Il faut bien comprendre que leur seule action est d'écrire les données dans l'exécutable à l'endroit même où elles se trouvent dans le code source.

Pour accéder à ces données (appelées « variables »), il suffit de connaître leur adresse. Pour cela, on peut les faire précéder d'un label de données.

Exemple :

TOTO dw 1982

Dans la ligne précédente, TOTO représente l'adresse du nombre 1982 qui est défini juste après. Comme ce nombre est codé sur deux octets (word = 16 bits), c'est le premier octet qui est adressé par TOTO.

Le compilateur se contentera d'écrire le nombre 1982 à l'endroit où se trouve la déclaration.

Remarque : en C, on écrirait ceci :

int TOTO = 1982;

En BASIC ou en PASCAL, cette définition n'a pas d'équivalent, puisque les variables ne peuvent être initialisées lors de leur déclaration. Il est donc obligatoire d'ajouter une ligne de code pour le faire.

Dans la partie précédente, nous avons eu à définir le message qui serait affiché à l'écran. Voici la ligne que nous avons écrite :

message db “Bonjour, monde !”, ‘$'

Lorsqu'il rencontre une chaîne de caractères, le compilateur écrit les codes ASCII de tous les caractères, les uns à la suite des autres.

Voici une autre manière d'écrire cette définition de données. Le résultat (i.e. le programme compilé) est EXACTEMENT LE MÊME :

message db 42h, 111, ‘nj'

db ‘our, mo'

db ‘nde !$'

Pour définir plusieurs fois à la suite les mêmes données, on utilise “DUP” (« duplicate ») de la manière suivante :

TOTO db 100 dup(“TOTO EST BEAU”)

Cela revient à écrire :

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

;… etc. (100 fois) …

Il est fréquent que de nombreuses données n'aient pas besoin d'être initialisées à une valeur précise. Dans ce cas, on les regroupe à la fin du programme et on les remplace par le caractère ‘?'. Les adresses des labels seront calculées de la même façon, mais aucune donnée ne sera écrite dans le fichier (et a fortiori dans la RAM). On dit que l'on met ses variables sur le « tas » (« heap » en anglais). La seule différence avec une définition classique est qu'au début de l'exécution, nos variables n'auront pas de valeur définie. Leurs valeurs seront « aléatoires » en ce sens qu'on ne peut les connaître au moment de la compilation.

Exemple :

Image non disponible

Remarque : Si elles ne sont pas regroupées en fin de programme, le compilateur sera obligé d'écrire les données dans le fichier afin de ne pas fausser les adresses des variables (ou du code) qui suivent. Il remplira alors les points d'interrogation avec des zéros.

Pour indiquer qu'un nombre est noté en base hexadécimale, on lui ajoute la lettre ‘h'. La lettre ‘b' signifie que le nombre est codé en binaire, la lettre 'o' en octal et la lettre ‘d' en base décimale (base par défaut).

IV-A-2. L'adressage

IV-A-2-a. L'adressage immédiat

On appelle « adressage immédiat » l'adressage qui ne fait intervenir que des constantes.

Considérons cette partie de programme qui stocke le nombre 125h dans une variable appelée TOTO, lui ajoute 15 puis charge le résultat dans AX.

Image non disponible

La ligne

mov word ptr ds:[TOTO], 125h

charge le nombre 125h dans le mot de la RAM adressé par DS et l'offset de “TOTO”. La ligne suivante ajoute 15 au contenu de ce mot.

L'expression “word ptr” devant l'adresse, obligatoire ici, indique la taille de la variable dans laquelle doit être stocké le nombre 125h.

Si on avait mis “dword ptr”, ce nombre aurait été codé sur 32 bits (00000125h) au lieu de 16 bits (0125h) : on aurait donc écrasé les deux octets qui suivent la variable. Si on avait mis “byte ptr”, la compilation aurait été impossible, car un octet ne peut contenir un nombre supérieur à FFh.

Le compilateur n'a en effet aucun moyen de connaître cette taille. La variable “TOTO” n'a pas de taille (malgré le mot “dw”) : ce n'est en fait qu'un pointeur vers le premier octet du word qu'elle représente.

En revanche, la troisième instruction ne fait pas apparaître l'expression “word ptr”. Cette fois-ci elle est facultative du fait que la destination (AX = 16 bits) impose la taille.

On peut évidemment remplacer “TOTO” par une constante numérique :

mov word ptr ds:[004Ch], 125h

Le mot 125h sera alors écrit à l'adresse DS:4C.

Une question se pose à présent : que se passe-t-il si le programmeur n'écrit pas le registre de segment dans l'adresse ?

C'est là qu'intervient la directive “assume”. Le compilateur va devoir trouver lui-même quel registre l'utilisateur a voulu sous-entendre.

Si on a utilisé un label, alors le segment sera celui dans lequel est déclaré le label. Mais le compilateur veut un REGISTRE de segment. Il va donc prendre celui qui est censé pointer vers le bon segment et pour le savoir, il examine la directive assume.

Voilà pourquoi cette dernière peut nous épargner d'écrire pour chaque variable l'expression “ds:”.

Comprenez bien que cette directive ne sert à rien d'autre qu'à cela et qu'en aucune façon elle ne force les registres de segment à prendre quelque valeur que ce soit.

IV-A-2-b. L'adressage indexé et/ou basé

Comme nous l'avons déjà expliqué, il est possible d'utiliser des registres de base ou d'index pour adresser un octet.

On appelle « base » les registres BX et BP et « index » les registres SI et DI. La différence est que la base est censée être fixe tandis que l'index varie automatiquement lorsqu'on utilise certaines opérations, telles que MOVSB.

Remarque : BX, BP, DI et SI peuvent tous être utilisés comme des registres généraux.

Voici les adressages possibles :

  • Constante seule (= adressage immédiat) :

Exemples :

MOV AH, byte ptr [TOTO]

MOV BX, word ptr ds:[1045h]

MOV ES:[10 + TOTO x 2], BL

  • Base seule :

Exemple :

MOV dword ptr ds:[BP], 15

  • Index seul :

Exemple :

MOV dword ptr es:[DI], 142

  • Base + Constante :

Exemples :

MOV byte ptr ds:[BP + 1], 12

MOV ds:[BX + (TOTO - BOBO)/10], AX

  • Index + Constante :

Exemples :

MOV CX, [TOTO + DI]

MOV AL, ds:[1 + SI]

  • Base + Index :

Exemple :

MOV ds:[BP + SI], AH

  • Base + Index + Constante :

Exemple :

MOV AX, word ptr [TOTO + BX + DI + 1]

Remarque : si la constante n'est pas un label, il est parfois impératif de spécifier le registre de segment ! Tout dépend du contexte…

IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS

IV-B-1. Les sauts inconditionnels

Ce paragraphe répond à la question : « Mais comment fait-on un GOTO en assembleur ? ».

L'instruction équivalente est JMP qui signifie « jump ».

La syntaxe est

JMP MonLabel

La référence à l'étiquette MonLabel sera remplacée lors de la compilation par la distance (signée) en octets qui sépare l'instruction qui suit immédiatement le JMP de l'instruction adressée par MonLabel. En pratique, le JMP s'utilise exactement comme un GOTO en BASIC.

Cette instruction permet de faire un saut inconditionnel : le programme fera ce saut quoi qu'il arrive.

Exemple :

Image non disponible

Le mot “short” ajouté après “jmp” indique au compilateur que le label “COUCOU” se trouve à une distance (signée) qui peut être stockée sur un seul octet. Le compilateur ne le sait pas encore lorsqu'il essaie de compiler l'instruction de saut, car le label se trouve en deçà de cette instruction. C'est pourquoi il prévoit deux octets pour écrire le saut, au cas où l'adresse d'arrivée est éloignée de plus de 128 octets. Lorsqu'il effectue une deuxième « passe », s'il s'aperçoit qu'un seul octet aurait suffi il remplit alors l'octet inutile avec l'instruction NOP (« No Operation ») qui ne fait rien du tout. Le programme marchera parfaitement (encore heureux !), mais cela gaspille un octet. Le programmeur a la possibilité d'aider le compilateur en ajoutant le mot “short”, ainsi un seul octet est prévu pour la distance de saut. Si le label est placé plus haut que l'instruction de saut, il est inutile de l'écrire.

Remarque : Ce mot peut également être utilisé avec les instructions de saut conditionnel que nous étudierons plus loin.

IV-B-1-a. Les procédures

IV-B-1-a-i. appels de procédures

Ceux qui ont déjà programmé en BASIC connaissent les instructions GOSUB et RETURN. Leurs équivalents en assembleur sont CALL et RET.

  • L'instruction CALL sert à appeler une procédure :

Syntaxe :

CALL MonLabel

Action : empile l'offset de l'instruction suivante puis fait un simple saut à l'adresse représentée par MonLabel.

  • RET (ou RETN) permet de retourner à l'instruction qui suit immédiatement le CALL :

Syntaxe :

RET

Action : dépile l'adresse de retour et la met dans IP. Le programme continue donc à l'adresse qui suit le CALL.

Remarque : Rappelez-vous qu'il est possible de terminer un programme COM avec l'instruction RET. Puisque le DOS empile un zéro de deux octets au chargement du programme, IP prendra la valeur 0000 lorsque le processeur exécutera cette instruction : il pointera donc vers le début du PSP. Or le PSP commence toujours par les deux octets suivants : CDh 20h (ce qui s'écrit INT 20h en assembleur). L'interruption 20h, qui permet de terminer un programme COM, sera donc appelée et le contrôle sera rendu au DOS.

Voici un exemple d'utilisation des procédures aussi simple que possible : ce programme COM appelle 12 fois une procédure qui écrit un message à l'écran et rend la main au DOS. Il n'est d'aucune utilité et n'est pas optimisé du tout.

Image non disponible

Remarque : Les codes ASCII 10 et 13 représentent respectivement la fin de ligne et le retour chariot. Grâce à eux, on revient à la ligne chaque fois qu'on a écrit le message.

Ce programme fonctionne parfaitement. Toutefois, les conventions d'écriture veulent que les procédures soient écrites comme suit :

Image non disponible

Le programme compilé est toujours le même, mais le code est plus lisible puisqu'on distingue les procédures des simples labels. Le mot “near” signifie que l'adresse de cette procédure est réduite à un offset. Il n'y a pas d'adresse de segment. La procédure ne pourra donc être appelée que de l'intérieur même du segment.

Le mot qui a le sens contraire de “near” est “far”. Les « call far » sont assez peu utilisés.

IV-B-1-a-ii. le passage des paramètres

Il existe deux moyens de passer des paramètres à une procédure : les registres et la pile.

  • Le passage par registres :

Il consiste à transmettre les paramètres dans des registres convenus à l'avance, par exemple AX et BX. Les deux gros avantages de cette méthode sont sa simplicité et la vitesse d'exécution. L'inconvénient majeur est le nombre limité de registres, d'autant plus qu'au moment de l'appel, certains d'entre eux ne seront peut-être pas disponibles.

  • Le passage par la pile :

C'est la méthode qu'utilisent les langages de haut niveau tels que le C ou le PASCAL. Avant l'appel, on empile les paramètres un à un. La procédure appelée se chargera de les lire.

Pour cela on utilise le registre BP de la manière suivante : on transfère la valeur de SP dans BP à l'aide de l'instruction “MOV BP, SP”. L'adresse de retour (qui sera dépilée quand le processeur rencontrera l'instruction “RET”) se trouve à l'adresse SS:[BP], le dernier paramètre est adressé par SS:[BP + 2], l'avant-dernier par SS:[BP + 4], etc. On peut ainsi lire n'importe quel paramètre sans le dépiler.

Attention cependant ! À la fin de la procédure, il faudra tout de même rendre à SP la valeur qu'il avait avant l'empilage des paramètres. Pour cela, on fait suivre l'instruction RET du nombre de paramètres, multiplié par 2 (car chaque paramètre comporte deux octets).

Exemple :

Image non disponible

Soyez toujours très vigilant avec les appels de procédures : pensez que l'adresse de retour sera dépilée lorsque le programme rencontrera l'instruction RET. Il serait donc suicidaire de laisser une donnée empilée avant d'appeler cette instruction.

IV-B-1-b. Les macros

Étant donné que certaines instructions se répètent constamment dans un programme, l'écriture de macrofonctions (ou macros) est un moyen pratique de rendre votre code source plus lisible.

Il est possible de choisir pour certaines suites d'instructions un nom qui les représente. Lorsque le compilateur (en fait, le préprocesseur) rencontrera ce nom dans votre code source, il le remplacera par les lignes de code qu'il désigne. Ces lignes forment une « macro ».

Les macros, à la différence des procédures, n'ont aucune signification pour la machine. Seul le compilateur comprend leur signification. Elles ne sont qu'un artifice mis à la disposition du programmeur pour clarifier son programme. Lorsque le compilateur rencontre le nom d'une macro dans votre code, il le remplace par le code de la macro. Tout se passe exactement comme si vous aviez tapé vous-même ce code à la place du nom de la macro.

Ainsi, si vous appelez quinze fois une macro dans votre programme, le compilateur écrira quinze fois le code de cette macro. C'est toute la différence avec les procédures qui ne sont écrites qu'une seule fois, mais peuvent être appelées aussi souvent qu'on veut à l'aide d'un CALL.

Voici comment écrire une macro : l'exemple suivant sert à écrire un message à l'écran.

Image non disponible

Le code précédent peut être écrit n'importe où dans votre programme, à condition qu'il se trouve avant tout appel de cette macro. Afin d'éviter les ennuis, il est fortement conseillé de réunir vos macros au début du code, avant toute autre ligne. De toute façon, il ne sera pas compilé à l'endroit où vous l'avez écrit, mais aux endroits où se trouvent les appels de macros.

Le mot “text?” est un « paramètre ». Le point d'interrogation n'est pas requis ; nous l'avons mis pour indiquer qu'il s'agit d'un paramètre et non d'une variable. Une macro peut utiliser plusieurs paramètres séparés par des virgules.

Pour appeler cette macro, il vous suffit d'écrire la ligne

ecrit_texte “Coucou ! Ceci est un essai !”

Le compilateur se chargera alors de la remplacer par les instructions comprises entre la première et la dernière ligne de cet exemple, en prenant le soin de remplacer le mot “text?” par le message fourni en paramètre.

En assembleur, chaque label doit avoir un nom unique. Or, si une macro est appelée plusieurs fois, les mêmes noms seront utilisés. Il faut donc la plupart du temps inclure la directive LOCAL qui forcera le compilateur à changer le nom des labels à chaque appel de la macro. Attention : cette directive doit suivre immédiatement la déclaration de la macro.

Supposons à présent que l'on veuille écrire à l'écran le message « Je suis bien content » et revenir à la ligne à l'aide de notre macro ecrit_texte.

La syntaxe suivante :

ecrit_texte “Coucou ! Ceci est un essai !”, 10, 13

est incorrecte, car le compilateur croirait que l'on a écrit trois paramètres ! Il faut alors entourer notre unique paramètre par les signes ‘<' et ‘>' :

ecrit_texte <”Coucou ! Ceci est un essai !”, 10, 13>

IV-B-1-c. La directive EQU

La directive EQU a un rôle voisin de celui des macros. Elle permet de remplacer un simple mot par d'autres plus complexes. Son intérêt est qu'elle peut être invoquée en plein milieu d'une ligne.

Quelques exemples :

Longueur EQU (fin - debut)

Message EQU “Bonjour messieurs ! Comment allez-vous ?”, ‘$'

Version EQU 2

Quitter EQU ret

Quitter2 EQU int 20h

Mettre_dans_AH EQU mov ah,

Interruption_21h EQU int 21h

Un programme qui contient de telles directives peut se terminer ainsi :

Mettre_dans_AH 4Ch

Interruption_21h

Ou ainsi (si c'est un COM) :

Quitter2

IV-B-1-d. L'inclusion de fichiers

Il est possible d'accéder à des procédures, des macros ou des définitions EQU qui se trouvent dans d'autres fichiers. Cela permet de se constituer des bibliothèques de macros ou de procédures que l'on peut réutiliser d'un programme à l'autre.

Pour inclure le fichier TOTO.LIB, écrivez au début de votre code source :

Image non disponible

La condition “if1” indique au compilateur que l'inclusion ne doit s'effectuer que lors de la première passe.

Le fichier TOTO.LIB est un fichier texte tout à fait banal qui contient des lignes de code en assembleur.

IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE

Remarques préliminaires :

  • Le principe du langage assembleur est de remplacer chaque opcode hexadécimal par un mot facile à retenir. Ce mot est appelé mnémonique. Par exemple, “INT” est le mnémonique associé à l'opcode CDh. Chaque fois que le compilateur rencontrera ce mot, il le remplacera par l'octet CDh et écrira ensuite l'opérande (ici : le numéro de l'interruption) en hexadécimal.
  • Cette liste récapitule les instructions que nous connaissons déjà et en présente de nouvelles. Elle n'est pas exhaustive, mais vous sera amplement suffisante pour la plupart de vos programmes.
  • Certaines instructions, comme PUSHA, ne sont disponibles que pour des modèles de processeurs plus évolués que le 8086, par exemple le 286. N'oubliez pas la directive .386 si vous les utilisez.

IV-C-1. L'instruction NOP (« No Operation »)

Syntaxe : NOP

Description : Ne fait rien ! Mais alors RIEN ! Que dalle ! Niet !

IV-C-2. L'instruction MOV (« Move »)

Syntaxe : MOV Destination, Source

Description : Copie le contenu de Source dans Destination.

Mouvements autorisés :

MOV Registre général, Registre quelconque

MOV Mémoire, Registre quelconque

MOV Registre général, Mémoire

MOV Registre général, Constante

MOV Mémoire, Constante

MOV Registre de segment, Registre général

Remarques : Source et Destination doivent avoir la même taille. On ne peut charger dans un registre de segment que le contenu d'un registre général (SI, DI et BP sont considérés ici comme des registres généraux).

Exemples :

MOV AX, 5

MOV ES, DX

MOV AL, [Variable1] ; Copie un octet, car AL contient 8 bits

MOV [Variable2], DS ; Copie un word car DS contient 16 bits

MOV word ptr [Variable3], 12 ; Ici, on spécifie que la variable est un word

IV-C-3. L'instruction XCHG (« Exchange »)

Syntaxe : XCHG Destination, Source

Description : Échange les contenus de Source et de Destination.

Mouvements autorisés :

XCHG Registre général, Registre général

XCHG Registre général, Mémoire

XCHG Mémoire, Registre général

IV-C-4. L'instruction JMP (« Jump »)

Syntaxe : JMP MonLabel

Description : Saute à l'instruction pointée par MonLabel.

IV-C-5. L'opérateur CMP (« Compare »)

Syntaxe : CMP Destination, Source

Description : Cet opérateur sert à comparer deux nombres : Source et Destination. C'est le registre des indicateurs qui contient les résultats de la comparaison. Ni Source ni Destination ne sont modifiés.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : Cet opérateur effectue en fait une soustraction, mais contrairement à SUB, le résultat n'est pas sauvegardé.

Le programme doit pouvoir réagir en fonction des résultats de la comparaison. Pour cela, on utilise les sauts conditionnels (voir ci-dessous).

IV-C-6. Les instructions de saut conditionnel

Les sauts conditionnels sont terriblement importants, car ils permettent au programme de faire des choix en fonction des données.

Un saut conditionnel n'est effectué qu'à certaines conditions portant sur les flags (par exemple : CF = 1 ou ZF = 0).

Certains mnémoniques de sauts conditionnels sont totalement équivalents, c'est-à-dire qu'ils représentent le même opcode hexadécimal. C'est pour aider le programmeur qu'ils existent parfois sous plusieurs formes.

IV-C-6-a. les sauts de comparaison

  • JE (« Jump if Equal ») fait un saut au label spécifié si et seulement si ZF = 1. Rappelez-vous que ce flag est à 1 si et seulement si le résultat de l'opération précédente vaut zéro. Comme CMP réalise une soustraction, on utilise généralement JE pour savoir si deux nombres sont égaux.

Exemple :

Image non disponible

Mnémonique équivalent : JZ (« Jump if Zero »)

  • JG (« Jump if Greater ») fait un saut au label spécifié si et seulement si ZF = 0 et SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur à un autre.

Exemple:

Image non disponible

Mnémonique équivalent : JNLE (« Jump if Not Less Or Equal »)

  • JGE (« Jump if Greater or Equal ») fait un saut au label spécifié si et seulement si SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNL (« Jump if Not Less »)

  • JL (« Jump if Less ») fait un saut au label spécifié si et seulement si SF <> OF. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNGE (« Jump if Not Greater Or Equal »)

  • JLE (« Jump if Less Or Equal ») fait un saut au label spécifié si et seulement si SF <> OF ou ZF = 1. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNG (« Jump if Not Greater »)

  • JA (« Jump if Above ») fait un saut au label spécifié si et seulement si ZF = 0 et CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur à un autre.

Mnémonique équivalent : JNBE (« Jump if Not Below Or Equal »)

  • JAE (« Jump if Above or Equal ») fait un saut au label spécifié si et seulement si CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNB (« Jump if Not Below »)

  • JB (« Jump if Below ») fait un saut au label spécifié si et seulement si CF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNAE (« Jump if Not Above Or Equal »)

  • JBE (« Jump if Below or Equal ») fait un saut au label spécifié si et seulement si CF = 1 ou ZF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNA (« Jump if Not Above »)

IV-C-6-a-i. les sauts de test sur les flags

Ces instructions testent un flag unique et exécutent ou non le saut selon la valeur de ce flag.

  • JC (« Jump if Carry ») fait un saut au label spécifié si et seulement si CF = 1.

Remarques : Ce mnémonique correspond au même opcode que JB. Il est souvent employé pour vérifier que l'appel d'une interruption n'a pas déclenché d'erreur.

Exemple :

Image non disponible

Si l'appel de la fonction 3Eh de l'interruption 21H se solde par une erreur, alors la CF vaudra 1 et le saut sera accompli. Dans le cas opposé, l'exécution continuera normalement de manière linéaire.

  • JNC (« Jump if not Carry ») fait un saut au label spécifié si et seulement si CF = 0.
  • JZ (« Jump if Zero ») fait un saut au label spécifié si et seulement si ZF = 1. Ce mnémonique correspond au même opcode que JE.
  • JNZ (« Jump if not Zero ») fait un saut au label spécifié si et seulement si ZF = 0. Ce mnémonique correspond au même opcode que JNE.
  • JS (« Jump if Sign ») fait un saut au label spécifié si et seulement si SF = 1.
  • JNS (« Jump if not Sign ») fait un saut au label spécifié si et seulement si SF = 0.
  • JO (« Jump if Overflow ») fait un saut au label spécifié si et seulement si OF = 1.
  • JNO (« Jump if not Overflow ») fait un saut au label spécifié si et seulement si OF = 0.
  • JP (« Jump if Parity ») fait un saut au label spécifié si et seulement si PF = 1.
  • JNP (« Jump if not Parity ») fait un saut au label spécifié si et seulement si PF = 0.
IV-C-6-a-ii. le saut de test sur le registre CX

JCXZ (« Jump if CX = Zero ») fait un saut au label spécifié si et seulement si CX = 0.

IV-C-6-b. Les instructions arithmétiques

IV-C-6-b-i. L'instruction INC (« Increment »)

Syntaxe : INC Destination

Description : Incrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

Exemple : INC CL

IV-C-6-b-ii. L'instruction ADD (« Addition »)

Syntaxe : ADD Destination , Source

Description : Ajoute Source à Destination.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : ADD byte ptr [ VARIABLE + DI], 5

IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)

Syntaxe : ADC Destination , Source

Description : Ajoute ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-iv. L'instruction DEC (« Decrement »)

Syntaxe : DEC Destination

Description : Décrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

IV-C-6-b-v. L'instruction SUB (« Substract »)

Syntaxe : SUB Destination , Source

Description : Soustrait Source à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)

Syntaxe : SBB Destination , Source

Description : Soustrait ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vii. L'instruction MUL (« Multiply »)

Syntaxe : MUL Source

Description : Effectue une multiplication d'entiers non signés .

  • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
  • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
  • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

Indicateurs affectés : CF, OF

Remarque : Source ne peut être une valeur immédiate.

Exemples : MUL CX

MUL byte ptr [ TOTO ]

IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)

Syntaxe : IMUL Source

IMUL Destination , Source

IMUL Destination , Source , Valeur

Description : Effectue une multiplication d'entiers signés .

IMUL Source :

  • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
  • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
  • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

IMUL Destination , Source : Multiplie Destination par Source et place le résultat dans Destination .

IMUL Destination , Source , Valeur : Multiplie Source par Valeur et place le résultat dans Destination .

Indicateurs affectés : CF, OF

IV-C-6-b-ix. L'instruction DIV (« Divide »)

Syntaxe : DIV Source

Description : Effectue une division euclidienne d'entiers non signés .

  • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
  • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
  • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

Indicateurs affectés : AF, OF, PF, SF, ZF

Remarque : Source ne peut être une valeur immédiate.

IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)

Syntaxe : IDIV Source

Description : Effectue une division euclidienne d'entiers signés .

  • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
  • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
  • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-xi. L'instruction NEG (« Negation »)

Syntaxe : NEG Destination

Description : Forme le complément à 2 de Destination , i.e. prend l'opposé de Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : À ne pas confondre avec NOT, qui forme le complément à 1.

IV-C-6-c. Les instructions logiques

IV-C-6-c-i. L'instruction NOT (« Logical NOT »)

Syntaxe : NOT Destination

Description : Effectue un NON logique bit à bit sur Destination (i.e. chaque bit de Destination est inversé).

IV-C-6-c-ii. L'instruction OR (« Logical OR »)

Syntaxe : OR Destination , Source

Description : Effectue un OU logique inclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Afin d'optimiser la taille et les performances du programme, on peut utiliser l'instruction “OR AX, AX” à la place de “CMP AX, 0”.
En effet, un OU bit à bit entre deux nombres identiques ne modifie pas Destination et est exécuté « infiniment » plus rapidement qu'une soustraction. Comme les flags sont affectés, les sauts conditionnels sont possibles.

IV-C-6-c-iii. L'instruction AND (« Logical AND »)

Syntaxe : AND Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)

Syntaxe : TEST Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat n'est pas conservé, donc Destination n'est pas modifié. Seuls les flags sont affectés.

Cet opérateur est souvent utilisé pour tester certains bits de Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Exemple :

IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)

Syntaxe : XOR Destination , Source

Description : Effectue un OU logique exclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Pour remettre un registre à zéro, il est préférable de faire “XOR AX, AX” que “MOV AX, 0”. En effet, le résultat est le même, mais la taille et surtout la vitesse d'exécution de l'instruction sont très largement optimisées.

IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)

Syntaxe : SHL Destination , Source

Description : Décale les bits de Destination de Source positions vers la gauche. Les bits les plus à droite sont remplacés par des zéros.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : SHL AX, 4 ;permet de multiplier par 16 de façon infiniment plus rapide que MUL

Mnémonique équivalent : SAL (« Shift Arithmetical Left »)

IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)

Syntaxe : SHR Destination , Source

Description : Décale les bits de Destination de Source positions vers la droite. Les bits les plus à gauche sont remplacés par des zéros.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Mnémonique équivalent : SAR (« Shift Arithmetical Right »)

IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)

Syntaxe : ROL Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. Le dernier bit à être sorti à gauche et à être rentré à droite est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)

Syntaxe : ROR Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la droite. Le dernier bit à être sorti à droite et à être rentré à gauche est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)

Syntaxe : RCL Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. CF est utilisé comme intermédiaire : chaque bit qui sort à gauche est placé dans CF, et le contenu de CF est ensuite réinséré à droite. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)

Syntaxe : RCR Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la droite. CF est utilisé comme intermédiaire : chaque bit qui sort à droite est placé dans CF, et le contenu de CF est ensuite réinséré à gauche. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-d. Les instructions de manipulation des flags

IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)

Syntaxe : CLC

Description : Met CF à 0.

Indicateurs affectés : CF

IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)

Syntaxe : STC

Description : Met CF à 1.

Indicateurs affectés : CF

IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)

Syntaxe : CLD

Description : Met DF à 0.

Indicateurs affectés : DF

IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)

Syntaxe : STD

Description : Met DF à 1.

Indicateurs affectés : DF

IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)

Syntaxe : CLI

Description : Met IF à 0.

Indicateurs affectés : IF

IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)

Syntaxe : STI

Description : Met IF à 1.

Indicateurs affectés : IF

IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)

Syntaxe : CMC

Description : Inverse CF.

Indicateurs affectés : CF

IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)

Syntaxe : LAHF

Description : Charge dans AH l'octet de poids faible du registre des indicateurs.

IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)

Syntaxe : SAHF

Description : Stocke les bits de AH dans le registre des indicateurs.

IV-C-6-e. Les instructions de gestion de la pile

IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)

Syntaxe : PUSH Source

Description : Empile le mot Source. SP est décrémenté de 2.

Remarques : Source ne peut être une valeur immédiate. Il est possible d'abréger votre code source en écrivant par exemple “PUSH AX BX BP”. Le compilateur écrira alors trois fois l'instruction PUSH du langage machine. Il est possible également d'empiler des doubles-mots.

IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)

Syntaxe : POP Destination

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans Destination. SP est incrémenté de 2.

IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)

Syntaxe : PUSHF

Description : Empile le registre des indicateurs. SP est décrémenté de 2.

Remarque : PUSHFD empile le registre des indicateurs codé sur 32 bits.

IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)

Syntaxe : POPF

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans le registre des indicateurs. SP est incrémenté de 2.

Indicateurs affectés : Tous

Remarque : POPFD est utilisé pour un registre des indicateurs codé sur 32 bits.

IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)

Syntaxe : PUSHA

Description : Empile AX, BX, CX, DX, BP, SI, DI et SP.

Remarque : PUSHAD est utilisé pour des registres de 32 bits.

IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)

Syntaxe : POPA

Description : Restaure AX, BX, CX, DX, BP, SI, DI et SP à partir de la pile.

Remarque : POPAD est utilisé pour des registres de 32 bits.

IV-C-6-f. Les instructions de gestion des chaînes d'octets

IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)

Syntaxe : MOVSB

Description : Copie l'octet adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Pour copier plusieurs octets, faire REP MOVSB (« Repeat Move String Byte »). Le nombre d'octets à copier doit être transmis dans CX de même que pour un LOOP.

Exemple :

Image non disponible
IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)

Syntaxe : SCASB

Description : Compare l'octet adressé par ES:DI avec AL. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarques : Pour comparer plusieurs octets, faire “REP SCASB” ou “REPE SCASB” (« Repeat until Egal »), ou encore “REPZ SCASB” (« Repeat until Zero »). Ces trois préfixes sont équivalents.
Le nombre d'octets à comparer doit être transmis dans CX. La boucle ainsi créée s'arrête si CX = 0 ou si le caractère pointé par ES:DI est le même que celui contenu dans AL (i.e. si ZF = 1). On peut ainsi rechercher un caractère dans une chaîne.
Pour répéter au contraire la comparaison jusqu'à ce que ZF = 0, c'est-à-dire jusqu'à ce que AL et le caractère adressé par ES:DI diffèrent, utiliser REPNE ou REPNZ.

Exemple :

Image non disponible
IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)

Syntaxe : LODSB

Description : Charge dans AL l'octet adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour MOVSB.

Syntaxe : STOSB

Description : Stocke le contenu de AL dans l'octet adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

Syntaxe : CMPSB

Description : Compare l'octet adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g. Les instructions de gestion des chaînes de mots

Ce sont les mêmes que les précédentes, hormis qu'elles traitent des mots et non des octets et que leur nom se termine par un ‘W' au lieu du ‘B'.

IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)

Syntaxe : MOVSW

Description : Copie le mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Pour copier plusieurs mots, faire “REP MOVSW”. Le nombre de mots à copier doit être transmis dans CX.

IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)

Syntaxe : SCASWs

Description : Compare le mot adressé par ES:DI avec AX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)

Syntaxe : LODSW

Description : Charge dans AX le mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)

Syntaxe : STOSW

Description : Stocke le contenu de AX dans le mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)

Syntaxe : CMPSW

Description : Compare le mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots

Elles traitent des doubles-mots et leur nom se termine par un ‘D'.

IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)

Syntaxe : MOVSD

Description : Copie le double-mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Pour copier plusieurs doubles-mots, faire “REP MOVSD”. Le nombre de doubles-mots à copier doit être transmis dans CX.

IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)

Syntaxe : SCASD

Description : Compare le double-mot adressé par ES:DI avec EAX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)

Syntaxe : LODSD

Description : Charge dans EAX le double-mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)

Syntaxe : STOSD

Description : Stocke le contenu de EAX dans le double-mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)

Syntaxe : CMPSD

Description : Compare le double-mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-i. Les instructions d'appel

IV-C-6-i-i. L'instruction CALL (« Procedure Call »)

Syntaxe : CALL MaProc

Description : Appel de procédure. Si MaProc se trouve dans un segment extérieur, le processeur empile CS. Ensuite, dans tous les cas, il empile IP et fait un saut à l'étiquette MaProc.

IV-C-6-i-ii. L'instruction RET (ou RETN)

Syntaxe : RET

Description : Retour de procédure se trouvant à l'intérieur du segment (NEAR). Un mot est dépilé et placé dans IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-i-iii. L'instruction RETF

Syntaxe : RETF

Description : Retour de procédure se trouvant à l'extérieur du segment (FAR). Deux mots sont dépilés et placés dans CS:IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-j. Les instructions de boucle

IV-C-6-j-i. L'instruction LOOP

Syntaxe : LOOP MonLabel

Description : Décrémente CX, puis, si CX <> 0, fait un saut à MonLabel.

IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)

Syntaxe : LOOPE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 1, fait un saut à MonLabel.

Mnémonique équivalent : LOOPZ

IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)

Syntaxe : LOOPNE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 0, fait un saut à MonLabel.

Mnémonique équivalent : LOOPNZ

IV-C-6-k. Les instructions d'adressage

IV-C-6-k-i. L'instruction LEA (« Load effective address »)

Syntaxe : LEA Destination, Source

Description : Charge l'offset de la source dans le registre Destination.

Exemple : LEA BP, word ptr [BP + TOTO]

IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)

Syntaxe : LDS Destination, Source

Description : Transfère dans DS:Destination le contenu de la mémoire adressée par Source.

IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)

Syntaxe : LES Destination, Source

Description : Transfère dans ES:Destination le contenu de la mémoire adressée par Source.

IV-C-6-l. Les instructions de conversion arithmétique

Remarque préliminaire : Nous n'expliciterons pas toutes ces instructions.

IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)

Syntaxe : CBW

Description : Convertit l'octet signé stocké dans AL en un mot (signé) stocké dans AX. Ainsi, si AL est négatif, AH sera rempli de 1 binaires, sinon, AH sera mis à 0.

IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)

Syntaxe : CWD

Description : Convertit le mot signé stocké dans AX en un double-mot (signé) stocké dans DX:AX. Ainsi, si AX est négatif, DX sera rempli de 1 binaires, sinon DX sera mis à 0.

IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)

Syntaxe : MOVSX Destination, Source

Description : Déplace le contenu signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. Si Source est négatif, la partie haute de Destination sera remplie de 1 binaires, sinon elle sera remplie de 0.

IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)

Syntaxe : MOVZX Destination, Source

Description : Déplace le contenu non signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. La partie haute de Destination sera donc mise à 0.

IV-C-6-l-xi. L'instruction XLAT (« Translate »)

IV-C-6-m. Les instructions d'entrée-sortie

IV-C-6-m-i. L'instruction IN (« Input from port »)

Syntaxe : IN Destination, Port

Description : Charge un octet ou un mot depuis un port d'entrée-sortie dans AL ou AX. Port peut être DX ou bien une constante de 8 bits.

IV-C-6-m-ii. L'instruction OUT (« Output to port »)

Syntaxe : OUT Port, Source

Description : Écrit dans Port la valeur contenue dans Source. Source ne peut être que AL ou AX. Port est une constante ou bien DX.

V. QUATRIÈME PARTIE

Remarque préliminaire : Cette partie présente quelques-unes des interruptions du DOS qui servent à manier les fichiers d'un disque. Nous la conclurons par l'étude d'un exemple de programme qui crypte un fichier choisi par l'utilisateur.

V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES

Sous DOS, il existe deux méthodes pour accéder aux fichiers et pour chacune d'elles un lot d'interruptions spécifiques. La première est la méthode des FCB (« File contol block »). Elle est rarement utilisée, aussi ne l'aborderons-nous pas. Nous étudierons le principe de la méthode des « handles ».

Pour lire ou écrire des données dans un fichier, il est nécessaire de l'ouvrir, c'est-à-dire de le charger en mémoire. Quand toutes les opérations de lecture et d'écriture auront été effectuées, le fichier devra être refermé afin d'enregistrer les éventuelles dernières modifications et surtout de libérer la mémoire occupée.

V-A-1. Ouverture d'un fichier

On ouvre un fichier en appelant la fonction 3dh de l'interruption 21h. Celle-ci attend comme paramètre dans DS:DX l'adresse d'une chaîne de caractères qui contient le chemin d'accès au fichier sur un disque, par exemple “C:\MonDoss\MonFic.txt”.

Remarque : il n'est pas indispensable de mentionner le chemin d'accès complet : par défaut, le fichier sera cherché à partir du dossier courant.

Remarque importante : La chaîne doit être impérativement suivie de l'octet 00h qui sert à marquer sa fin.

Il nous faut également spécifier le mode d'accès en écrivant dans AL un 0 (si on veut ouvrir le fichier en lecture seule), un 1 (si on veut l'ouvrir en écriture seule) ou un 2 (lecture ET écriture).

Si l'interruption échoue, le flag CF sera mis à 1 sans que le fichier soit ouvert. Dans le cas contraire, CF est mis 0 et le registre AX contient un petit nombre entier (par exemple 5) appelé « handle » (ce qui signifie « poignée ») du fichier. Ce handle représente le fichier. C'est lui qu'il faudra désormais invoquer pour effectuer des opérations de lecture ou d'écriture, et non pas le chemin d'accès.
En effet, les chemins d'accès ne sont donc plus d'aucune utilité puisque les fichiers sont ouverts dans la mémoire vive.

V-A-2. Lecture dans un fichier

Une fois le fichier ouvert, on peut le lire avec la fonction 3fh. Il suffit de mentionner le handle dans BX, le nombre d'octets à lire dans CX, et l'adresse d'un buffer dans DS:DX.
Au cas où vous ne sauriez pas ce qu'est un buffer (ou tampon), sachez que c'est simplement une variable (généralement une chaîne de caractères) destinée à recevoir des données (ou à en fournir). Dans notre cas, le buffer va recevoir les octets lus dans le fichier.

Après l'appel, AX contient le nombre d'octets qui ont été effectivement lus (il peut être inférieur à la taille demandée si le fichier n'est pas assez long). En cas de problème, CF sera mis à 1.

V-A-3. Écriture dans un fichier

Pour écrire des données, on procède de même avec la fonction 40h. Les paramètres sont les mêmes que pour la fonction 3fh. Le buffer contient cette fois les octets à écrire. Après l'appel, le nombre d'octets qui ont été effectivement écrits est stocké dans AX (il sera être inférieur à la taille spécifiée si le disque est plein).

Les données sont écrites sur le disque dur que le tampon (dans la mémoire vive) est plein.

V-A-4. Existence d'un pointeur de fichier

Une question se pose cependant : à quel endroit du fichier les données sont-elles lues (ou écrites) ?

Réponse : quand un fichier est ouvert, un pointeur spécial pointant vers le début du fichier est créé. La première opération de lecture (ou d'écriture) se fera donc au début du fichier.
Mais entre chaque opération, le pointeur est incrémenté de la taille des données que l'on a lues (ou écrites). La deuxième opération se fera donc sur les octets qui suivent ceux de la première.

Remarque : Il est possible de modifier directement le pointeur de fichier : voyez pour cela la fonction 42h…

V-A-5. Fermeture d'un fichier

Pour terminer, le fichier doit être refermé. Les modifications éventuellement apportées et non enregistrées seront écrites sur le disque, et le handle sera libéré. C'est la fonction 3eh qui se charge de tout cela. Elle attend simplement le handle du fichier dans BX. Et comme d'habitude, CF vaut 1 après l'appel si des erreurs ont été rencontrées.

Remarque : Attention lorsque vous laissez le fichier ouvert longtemps afin d'y ajouter progressivement des données ! Si le système plante, vous perdrez les données qui se trouvent dans le tampon à ce moment. C'est pourquoi il est conseillé de forcer régulièrement l'écriture sur le disque en refermant le fichier.

V-A-6. Conclusion

Le tableau suivant récapitule ces différentes étapes :

Fonction | Description | Paramètres
3d h | Ouvrir le fichier | - DS:DX : adresse d'une chaîne contenant le chemin d'accès - AL : mode d'accès
3e h | Fermer le fichier | - BX : handle
3f h | Lire le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer
40 h | Écrire dans le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer

V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS

Pour rechercher un fichier (ou un dossier), on se sert des fonctions 4eh (« Find First ») et 4fh (« Find Next »).

Imaginons par exemple que nous voulions chercher dans le dossier courant tous les fichiers qui portent l'extension “ .com ” afin de les supprimer. Comment devons-nous nous y prendre ?

V-B-1. La fonction 4eh

La fonction 4eh sert à définir des critères de recherche et à trouver le premier fichier qui correspond à ces critères (s'il existe).

On doit passer dans DS:DX l'adresse de la chaîne de caractères qui contient le masque de recherche (dans notre exemple, ce masque est “ *.com ”). Par défaut, les fichiers sont cherchés dans le dossier courant. Mais on peut évidemment spécifier un autre chemin dans le masque.

Remarque importante : afin que le DOS puisse connaître sa taille, le masque doit impérativement être terminé par l'octet 00h !

On écrit également dans CX les attributs des fichiers que l'on désire trouver. Si CX vaut 0, seuls les fichiers « normaux » pourront être trouvés. En fait, chaque bit de CL représente un attribut, comme le montre le tableau ci-dessous :

Bit | Signification
1 | Lecture seule
2 | Fichier caché
3 | Fichier système
4 | Volume
5 | Répertoire
6 | Fichier
7 | (Aucune…)
8 | (Aucune…)

Pour demander à la fonction 4eh de ne pas oublier les fichiers cachés, il suffit donc de charger CX avec la valeur 2 (bit numéro 2 = 1). De même, l'attribut 00000111 b (soit 7) nous permettra de trouver les fichiers en lecture seule, les fichiers cachés et les fichiers systèmes.

Remarque : Ne vous souciez pas trop des bits numéro 4 et 6. Laissez-les à 0.

Une fois que les paramètres ont été ajustés, on peut appeler la fonction 4eh. Si aucun fichier n'a été trouvé, le flag CF est mis à 1 . On doit donc faire un test sur CF pour savoir si la recherche peut continuer ou si elle doit s'arrêter.
Si au contraire la fonction a trouvé un fichier, les caractéristiques de ce fichier (i.e. son nom, sa taille, ses attributs…) sont inscrites dans une zone de la mémoire appelée DTA (« Disk Transfer Area »).

Mais où se trouve donc cette DTA et à quoi ressemble-t-elle ?

Réponse : par défaut, le DOS place la DTA dans le PSP de votre programme, à l'offset 80h.

Remarque : il vous est naturellement possible de la déplacer en faisant appel à la fonction 1ah.

Voici la structure de la DTA :

Adresse | Description | Taille (octets)
00 h | Lettre du lecteur (0=courant, 1=A, 2=B…) sur lequel se trouve le fichier | 1
01 h | Modèle de la recherche | 11
0C h | Réservé | 9
15 h | Attributs du fichier | 1
16 h | Heure du fichier | 2
18 h | Date du fichier | 2
1A h | Taille du fichier | 4
1E h | Nom du fichier avec l'extension | 13

Puisque par défaut la DTA est située dans le PSP à l'offset 80h, le nom du fichier trouvé est écrit à l'offset 80h + 1eh = 9eh. De même, la taille se trouve à l'offset 9ah, etc.

V-B-2. La fonction 4fh

Jusqu'à présent, nous n'avons trouvé qu'un seul fichier ! Pour poursuivre la recherche, appelez la fonction 4fh sans écrire aucun paramètre dans les registres. Les caractéristiques de votre recherche ont été mémorisées dans la DTA : vous n'avez donc pas à les rappeler. De même que pour la fonction 4eh, CF est mis à 1 si aucun nouveau fichier n'a été trouvé. C'est le signe que vous pouvez arrêter votre recherche.

V-B-3. Conclusion

Résumons-nous :

Fonction | Description | Paramètres
4e h | Trouver le premier fichier qui correspond aux caractéristiques spécifiées | - DS:DX : adresse d'une chaîne contenant le masque - CX : attributs
4f h | Trouver le prochain fichier | Aucun !

À titre d'exemple, écrivons à présent le programme que nous évoquions tout à l'heure : “ trouver tous les fichiers COM du dossier courant et les effacer ”.

V-B-4. EXEMPLE DE PROGRAMME

V-B-5. Entrée bufferisée au clavier

L'exemple que nous proposons ici fait appel à une saisie au clavier. Comme nous n'en avons pas encore rencontré, ce paragraphe explique brièvement comment utiliser les fonctions 0ah et 0ch.

On peut se servir de la fonction 0ah pour lire une chaîne de caractères au clavier. Le seul paramètre à fournir est l'adresse d'un buffer. Comme il est souvent indispensable d'effacer le buffer avant la lecture, il est préférable de ne pas utiliser la fonction 0ah directement, mais d'appeler la fonction 0ch. En effet, celle-ci commence par effacer le buffer, puis appelle la fonction 0ah (ou une fonction voisine dont le numéro doit être transmis dans AL).

Voici le code à écrire :

Image non disponible

La seule difficulté est dans la déclaration du buffer. Voici comment vous devez vous y prendre si vous considérez que l'utilisateur pourra entrer au plus n caractères :

Image non disponible

Le premier octet de “maChaine” doit contenir le nombre d'octets maximal qui pourront être entrés, plus 1. Pourquoi plus 1 ? Tout simplement parce que le DOS met à la fin des caractères tapés le code ASCII 13 (retour chariot).

Pour comprendre la suite de la déclaration, il faut savoir de quelle manière le DOS transmet les caractères qui ont été lus. Après la lecture, le deuxième octet du buffer contiendra le nombre exact d'octets lus (sans compter le 13 final). La chaîne proprement dite ne commencera donc qu'au troisième octet. Puisqu'un octet est utilisé pour donner au DOS le nombre maximal de caractères autorisés, un autre pour recevoir le nombre de caractères lus effectivement, et encore un autre pour recevoir le code ASCII 13, le buffer doit comporter trois octets de plus que la taille maximale de la chaîne. C'est pourquoi on écrit :

maChaine db n+1, ?, n dup(?), ?

V-B-6. Le programme

Le programme suivant demande à l'utilisateur d'entrer le nom d'un fichier se trouvant dans le dossier courant puis crypte ce fichier en appliquant un NOT logique sur chaque octet. Naturellement, il n'est pas optimisé du tout. Ce n'est qu'un exemple !

Image non disponible

Suggestion : quand vous aurez compris ce petit programme, essayez donc de l'améliorer ! Par exemple, il faudrait que l'utilisateur puisse taper le nom du fichier à crypter directement comme paramètre du programme (i.e. “CRY MONFICH.EXE”). Voyez pour cela la structure du PSP telle que présentée dans la première partie. De plus, le programme devrait pouvoir trouver automatiquement le nom du fichier d'arrivée : ce serait le même que celui du fichier à crypter, mais avec une extension différente (par exemple “.cry”).
Vous pourriez également étendre les possibilités du logiciel en permettant à l'utilisateur de saisir non plus un simple nom de fichier, mais un masque complexe avec des chemins d'accès et des caractères “jokers” comme '*' et '?'. Vous aurez donc besoin des fonctions de recherche 4eh et 4fh. L'utilisateur doit de surcroît pouvoir choisir s'il veut crypter un fichier ou bien le décrypter…

VI. CONCLUSION

Voilà. Nous espérons que vous avez compris l'essentiel de ce cours, à savoir la logique de la programmation en assembleur. Les connaissances que vous avez acquises en lisant la fin du cours devraient vous permettre de mieux saisir les premiers chapitres. C'est pourquoi nous vous invitons à relire ce tutoriel.

Certaines notions doivent encore vous paraître obscures. Maintenant que vous vous êtes familiarisé avec l'assembleur, commencez par écrire et tester quelques petits programmes COM inutiles et très simples. Vous verrez qu'au bout de quelques essais, tout ce que vous avez lu vous semblera très concret.

Lorsque vous aurez écrit vous-même quelques tout petits programmes qui fonctionnent, l'assimilation du langage deviendra alors très rapide, car l'assembleur est un langage très logique et très cohérent.

Nous vous conseillons d'écrire une bibliothèque de petites macros ou procédures qui vous serviront dans tous vos programmes. Par exemple, vous pouvez faire une macro qui affiche un entier à l'écran, ou bien qui convertit une chaîne de caractères en un nombre entier.

Voici un exemple de macro qui renvoie dans AL le nombre de chiffres d'un entier non signé de deux octets passé dans AX :

Image non disponible

N.B. Pensez avant toute chose à vous munir d'une liste des interruptions du DOS. Vous ne pourrez rien faire si vous n'en avez pas !

Bon courage et bonne chance !

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Benoît-M. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique Assembleur : Alcatîz -
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Bible du cracker
Cours d'assembleur par Falcon

SOMMAIRE

1 - Les bases indispensables pour débuter

A - Définition de l'assembleur
B - Le language hexadécimal
C – Le calcul binaire
D – Conversion binaire ó hexadécimal
E - Le processeur et ses registres

a ) Les registres généraux.
b ) Les registres pointeurs ou d'offset
c ) Le processeur et ses registres de segment
d ) Le registre Flag

2 - Les premières instructions

A - La première instruction : MOV
B - Une autre instruction : JMP
C - Quelques instructions arithmétiques : ADD et SUB

3 - Pile - Interruptions

A - La pile (Stack) et ses instructions

a ) PUSH.
b ) POP.

B - Les interruptions - Instructions

4 - Les flags - Les sauts conditionnels - CMP

A - Les flags - Les indicateurs

a ) CF
b ) PF
c ) AF
d ) ZF
e ) SF
f ) IF
g ) DF
h ) OF

B - Les instructions conditionnelles

JB - JNAE - JC
JAE - JNB - JNC
JE - JZ
JNE - JNZ
JO - JNO
JP - JPE
JNP - JPO
JS - JNS
JA - JNBE
JBE - JNA
JG - JNLE
JGE - JNL
JL - JNGE
JLE - JNG

C - L'instruction CMP

5 - Instructions mathématiques

A - Les instructions mathématiques

a ) MULTIPLICATION : MUL / IMUL
b ) DIVISION : DIV / IDIV
c ) SHR et SHL
d ) NEG

B - Les nombres à virgules
C - Les nombres négatifs
D - Les instructions logiques

a ) AND
b ) OR
c ) XOR
d ) NOT
e ) TEST

6 - La mémoire et ses instructions

7 - Les instructions assembleur

8 - Table ASCII

L’ASSEMBLEUR

1 - Les bases indispensables pour débuter

Pour cracker n’importe quel logiciel, il est indispensable de connaître le fonctionnement de l’assembleur et ses instructions.
Pour cela, je vous conseille vivement d’acheter les 2 livres suivants :
Assembleur " Une découverte pas à pas " de Philippe Mercier - Edition Marabout n°885 (environ 50 francs).
Assembleur " Théorie, pratique et exercices " de Bernard Fabrot - Edition Marabout n°1087 (environ 50 francs).

Comme vous le verrez, ce cours est surtout destiné à la programmation en asm.

Retour au sommaire

A - Définition de l'assembleur

L'assembleur est un language de programmation transformant un fichier texte contenant des instructions, en un programme que le processeur peut comprendre (programme en language machine).

Ce language machine a la particularité d'être difficile à programmer car il n'est composé que de nombres en hexadécimal (base 16). L'assembleur est une "surcouche" du language machine, il permet d'utiliser des instructions qui seront transformées en language machine donc il présente une facilité de programmation bien plus grande que le language machine. Le fichier texte qui contient ces instructions s'appelle le source.

Retour au sommaire

B - Le language hexadécimal

Nous allons aborder un aspect très important de la programmation en assembleur : le système de numérotation en hexadécimal.
Ce système est basé sur l'utilisation des chiffres et de certaines lettres de l'alphabet (de A à F). Vous connaissez bien entendu le système décimal (base 10).
En assembleur, les nombres décimals sont suivis d'un "d" (1000=1000d) mais en principe la majorité des assembleurs calculent en décimal par défaut.
La notation hexadécimale (base 16) implique qu’il faut disposer de 16 signes alignables dans une représentation et, comme les chiffres ne suffisent plus, on a décidé que les signes de 0 à 9 seraient représentés pas les chiffres 0..9 et les signes manquants pour obtenir 16 signes seraient les 6 premières lettres de l’alphabet soit A, B, C, D, E, F avec :

Hexadécimal | Décimal
A | 10
B | 11
C | 12
D | 13
E | 14
F | 15

Nous ne pouvons pas utiliser le G et les lettres qui suivent, donc nous augmenterons le premier chiffre ce qui donne 16d=10h. Continuez ainsi jusqu'à 255 qui fait FF. Et après. Et bien oncontinue. 256d=0100h (le h et le zéro qui précède indiquent que ce nombre est en hexadécimal). 257d=101h. 65535=FFFFh. Bon, je pense que vous avez compris.

Pour convertir des nombres du décimal en hexadécimal, vous pouvez utiliser la calculatrice de Windows en mode scientifique.

Exemples :
8 = 8*1
78 = 7*16 + 8*1
A78 = 10*256 + 7*16 + 8*1
EA78 = 14*4096 + 10*256 + 7*16 + 8*1

Retour au sommaire

C - Le calcul binaire

Je vais finir mon explication avec le calcul binaire. Vous savez probablement que les ordinateurs calculent en base 2 (0 ou 1). Le calcul binaire consiste à mettre un 1 ou un 0 selon la valeur désirée. Chaque 0 ou 1 est appelé un bit, 8 bits forment un octet.

Pour le nombre 1, nous avons 00000001b (le b signifie binaire). Un nombre binaire se décompose comme-ceci :
128|64|32|16|8|4|2|1
Pour le nombre 1, la première case est remplie (1=rempli). Pour avoir le nombre 2, nous aurons 00000010b et pour le nombre 3, nous aurons 00000011 (1 case=1 - 2ème case=1 => 1+2=3).
Le nombre 11111111b donnera 255 puisque en additionnant 1+2+4+...+128, on obtient 255. Plus il y a de bits, plus le nombre peut être grand, nous verrons plus tard qu'il existe différentes unités selon le nombre de bits.
Exemples :

Base 2 | Base 10
00000001 | 1 = 1*1
00000011 | 3 = 1*2 + 1*1
00001011 | 11 = 1*8 + 0*4 * 1*2 + 1*1
00011011 | 27 = 1*16 + 1*8 + 0*4 + 1*2 + 1*1

Retour au sommaire

D - Conversion binaire ó hexadécimal

Voici un exemple démontrant la simplicité de conversion d’un nombre binaire en hexadécimal :

Soit le nombre hexadécimal EA78 à convertir en binaire, 16 étant 2 à la puissance 4, chaque signe de la représentation hexadécimale sera converti en 4 signes binaires et le tour sera joué :

Hexadécimal | Décimal | Binaire
E | 14 | 1110
A | 10 | 1010
7 | 7 | 0111
8 | 8 | 1000

En alignant les représentations binaires obtenues, on trouve le nombre binaire suivant : 1110101001111000.
Inversement, pour convertir un nombre binaire en hexadécimal, c’est tout aussi simple, on regroupera les bits par groupe de 4 :
Soit 1110101001111000 à traduire en hexadécimal :
On le découpe d’abord en 4 : 1110 1010 0111 1000

Binaire | Décimal | Hexadécimal
1110 | 14 | E
1010 | 10 | 10
0111 | 7 | 7
1000 | 8 | 8

Retour au sommaire

E - Le processeur et ses registres

Le processeur est composé de différentes parties. Les registres sont les éléments les plus importants du processeur pour celui qui programme en asm. Les registres sont souvent représentés comme des cases dans lesquelles sont stockées différentes valeurs de différentes tailles selon le type et le nom du registre. Il existe plusieurs types de registres :

  • registres généraux ou de travail

ils servent à manipuler des données, à transférer des paramètres lors de l’appel de fonction DOS et à stocker des résultats intermédiaires.

  • registres d’offset ou pointeur

ils contiennent une valeur représentant un offset à combiner avec une adresse de segment

  • registres de segment

ils sont utilisés pour stocker l’adresse de début d’un segment. Il peut s’agir de l’adresse du début des instructions du programme, du début des données ou du début de la pile.

  • registre de flag

il contient des bits qui ont chacun un rôle indicateur.

Retour au sommaire

a ) Les registres généraux.

On a plusieurs registres généraux (de travail) qui commencent par A,B,C et D. Ces quatre registres sont les plus utilisés.
Le 1er, AX (registre de 16 bits) qui se divise en deux petits registres de 8 bits, AL (l=low=bas) et AH (h=high=haut).
Il est utilisé lors d’opérations arithmétiques.
Nous avons ensuite BX (BL et BH), CX (CL et CH) et DX (DL et DH), ces registres sont divisés comme AX en 2 parties hautes et basses.
On peut rajouter un "E" devant les registres 16 bits afin d'obtenir un registre 32 bits.
Ce qui donne EAX,EBX,ECX et EDX. Notez que l'on ne peut avoir de EAH ou ECL. La partie haute d'un registre 32 bits, n'est pas directement accessible, on doit utiliser différentes instructions afin de la faire "descendre" dans un registre de 16 bits et pouvoir finalement l'utiliser.
Ces registres peuvent contenir une valeur correspondant à leur capacité.
AX=65535 au maximum (16 bits) et AL=255 au maximum (8 bits). Je répète que la partie haute du registre 32 bits ne peut pas être modifiée comme un registre. Elle peut être modifiée seulement si l'on modifie tout le registre 32 bits (y compris la partie basse), ou par le biais de quelques instructions qui permettent de copier la partie basse du registre dans la partie haute, mais cela ne nous concerne pas pour l'instant.
Les processeurs 286 et moins ne peuvent pas utiliser les registres 32 bits (EAX,EBX,ECX,EDX = impossible). Ce n'est qu'à partir du 386 que l'utilisation du 32 bits est possible.

Retour au sommaire

b ) Les registres pointeurs ou d'offset

Pour terminer, je vais parler des registres pointeurs qui sont DI (destination index), SI (source index), BP (base pointer), IP (instruction pointer) et SP (stack pointer).

Ces registres peuvent eux aussi être étendus à 32 bits en rajoutant un "E" (EDI,ESI,EIP,ESP,EBP).

Ces registres n'ont pas de partie 8 bits, il n'y a pas de EDL ou de ESH.

  • SI est appelé " Source Index ". Ce registre de 16 bits est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est associé au registre de segment DS.
  • DI est appelé " destination Index ". Ce registre de 16 bits est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est normalement associé au registre de segment DS ; dans le cas de manipulation de chaînes de caractères, il sera associé à ES.
  • BP ou " Base Pointer ". Ce registre de 16 bits est associé au registre de segment SS (SS :BP) pour accéder aux données de la pile lors d’appels de sous-programmes (CALL).
  • SP ou " Stack Pointer ". Ce registre de 16 bits est associé au registre de segment SS (SS :SP) pour indiquer le dernier élément de la pile.
  • IP est appelé " Instruction Pointer ". Ce registre de 16 bits est associé au registre de segment CS (CS :IP) pour indiquer la prochaine instruction à exécuter. Ce registre ne pourra jamais être modifié directement ; il sera modifié indirectement par les instructions de saut, par les sous-programmes et par les interruptions.

Les registres SI et DI sont le plus souvent employés pour les instructions de chaîne et pour accéder à des blocs en mémoire.

Le registre SP est utilisé pour accéder à la pile.

Nous ne l'employons presque pas, sauf pour du code très spécifique.

Ces registres (sauf IP) peuvent apparaître comme des opérandes dans toutes les opérations arithmétiques et logiques sur 16 bits.

Retour au sommaire

c ) Le processeur et ses registres de segment

Pour pouvoir chercher et placer des choses dans sa mémoire, un ordinateur a besoin de ce qu'on appelle une adresse. Celle-ci se compose de deux parties de 32 bits (ou 16 bits, cela dépend du mode employé). La première est le segment et la deuxième, l'offset.

Voici un exemple :

0A000h:00000h (adresse de la mémoire vidéo)

Dans cet exemple, il s'agit d'un adressage 16 bits. L'adressage 16 bits s'utilise en mode _réel_. Le mode réel est un état du processeur où il ne peut accéder à des blocs de taille supérieure à 16 bits (65536 bytes). Le 286 introduira un mode dit protégé qui permet de franchir cette barrière pour pouvoir accéder à quasiment tout la RAM en un bloc (ce mode protégé sera grandement amélioré à partir du 386).

Nous nous contenterons du mode réel au début, car il est plus simple mais il présente tout de même des limites. Sur 386, les segments ont été conservés pour garder une certaine comptabilité avec les vieilles générations de PC.

Les registres de segment ne sont que lus et écris sur 16 bits. Le 386 utilise un offset de 32 bits (sauf en mode réel où il travaille toujours en 16 bits). Voici la liste de ces segments, ils sont au nombre de 6 :

  • CS (code segment) = segment de code

Ce registre indique l’adresse du début des instructions d’un programme ou d’une sous-routine

  • SS (stack segment) = segment de pile

Il pointe sur une zone appelée la pile. Le fonctionnement de cette pile seront expliqués au chapitre 3.

  • DS (data segment) = Segment de données

Ce registre contient l’adresse du début des données de vos programmes. Si votre programme utilise plusieurs segments de données, cette valeur devra être modifié durant son exécution.

  • ES (extra segment) = Extra segment

Ce registre est utilisé, par défaut, par certaines instructions de copie de bloc. En dehors de ces instructions, le programmeur est libre de l’utiliser comme il l’entend.

  • FS (extra segment - seulement à partir du 386) = segment supplémentaire
  • GS (extra segment - seulement à partir du 386) = segment supplémentaire

Ces deux derniers registres ont un rôle fort similaire à celui du segment ES

Retour au sommaire

d ) Le registre Flag

Le registre FLAG est en ensemble de 16 bits. La valeur représentée par ce nombre de 16 bits n’a aucune signification en tant qu’ensemble : ce registre est manipulé bit par bit, certains d’entre eux infuenceront le comportement du programme. Les bits de ce registres sont appelés " indicateurs ", on peut les regrouper en deux catégories :

  • Les indicateurs d’état :
Bit | Signification | Abréviation
0 | « carry » ou retenue | CF
2 | parité | PF
4 | retenue auxiliaire | AF
6 | zéro | ZF
7 | signe | SF
11 | débordement (overflow) | OF
  • Les indicateurs de contrôle :
Bit | Signification | Abréviation
8 | trap | TF
9 | interruption | IF
10 | direction | DF

Les bits 1, 5, 12, 13, 14, 15 de ce registre FLAG de 16 bits ne sont pas utilisés.

Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs. Les instructions conditionnelles testent la valeur des indicateurs et agissent en fonction du résultat. La description détaillée des flags est faite au chapitre 4.

Retour au sommaire

2 - Les premières instructions

A - La première instruction : MOV

Cette instruction vient de l'anglais "move" qui signifie DEPLACER mais attention, le sens de ce terme est modifié car l'instruction MOV ne déplace pas mais place tout simplement.

Cette instruction nécessite deux opérandes (deux variables) qui sont la destination et la source. Ceux-ci peuvent être des registres généraux ou des emplacements mémoire. Cependant, les deux opérandes ne peuvent pas être toutes les deux des emplacements mémoire. De même, la destination ne peut pas être ce qu'on appelle une valeur immédiate (les nombres sont des valeurs immédiates, des valeurs dont on connaît immédiatement le résultat) donc pas de MOV 10,AX. Ceci n'a pas de sens, comment pouvez-vous mettre dans le nombre 10, la valeur de AX ? 10 n'est pas un registre.

Une autre règle à respecter, les opérandes doivent être de la même taille donc pas de MOV AX,AL, cela me semble assez logique. On ne peut pas utiliser une valeur immédiate avec un registre de segment (MOV ES,5 = impossible mais MOV ES,AX = possible).

Le registre DS est le segment de données par défaut.

Ainsi au lieu de :

MOV AX, DS :[BX]

MOV AX, DS :[SI+2]

…

On aura :

MOV AX, [BX]

MOV AX, [SI+2]

…

Quelques exemples :

MOV AL,CL (place le contenu de CL dans AL) donc si AL=5 et CL=10, nous aurons AL=10 et CL=10.

MOV CX,ES:[DI] (place dans CX, le contenu 16 bits de l'emplacement ES:[DI]). Donc si le word (le word est l'unité correspondant à 16 bits) ES:[DI]=34000, nous aurons CX=34000.

MOV CL,DS:[SI] (place dans CL, le contenu 8 bits (byte) de l'emplacement DS:[SI]).

Donc si DS:[SI] (attention en 8 bits) = 12, CL vaudra 12.

Au sujet de ces emplacements mémoires, il faut que vous vous les représentiez comme des "cases" de 8 bits, comme le plus petit registre fait 8 bits, on ne peut pas diviser la mémoire en emplacements plus petits. On peut prendre directement 16 bits en mémoire (un word) ou carrément un dword (32 bits). Il est très important de saisir cette notion. Un exemple concret : vous avez des livres sur une étagère, le livre se trouvant toute à gauche est le 1er de la liste, nous nous déplaçons de gauche à droite.

Un livre est un byte (8 bits), 2 livres sont un word (16 bits), 4 livres font un dword (32 bits). Si nous faisons MOV AX,ES:[DI], nous allons prendre 2 livres, en commençant par le livre se trouvant à l'emplacement DI et en prenant ensuite le livre se trouvant à l'emplacement DI+1 (l'emplacement suivant dans votre rangée de livre, 1 byte plus loin en mémoire).

Voici un autre exemple :

MOV EAX,ES:[DI] (place un dword depuis ES:[DI] dans EAX). C'est comme si on copiait 4 livres dans EAX.

Et quelques exemples incorrects :

MOV 1,10 (impossible et pas logique)

MOV ES:[DI],DS:[SI] (incodable malheureusement)

MOV ES,10 (incodable, il faut passer par un registre général donc MOV ES,AX ou MOV ES,CX mais pas de MOV ES,AL).

Ce qu'il faut retenir c'est que l'instruction MOV est comme le signe '=' MOV ES,CX c'est comme faire ES=CX.

Retour au sommaire

B - Une autre instruction : JMP

JMP est une simplification pour exprimer le mot JUMP qui signifie SAUTER en anglais.

JMP va servir dans le programme pour "passer" d'une opération à une autre, de sauter dans le programme à différents endroits pour effectuer d'autres tâches.

Je ne vais pas m'attarder dans des explications sur le pointeur d'instruction mais sachez que chaque instruction est désignée par un emplacement en mémoire (défini par CS:[IP]).

L'instruction JMP va donc modifier la valeur de IP et par conséquence changer d'instruction.

On peut utiliser JMP avec des valeurs immédiates.

Exemples :

JMP 10 (il aura pour effet de passer, non pas à l'instruction qui se trouve à la ligne 10 , mais à l'emplacement mémoire 10. Une instruction n'est pas forcément de taille 1, certaines peuvent s'étaler sur plusieurs bytes.)

Il est plus simple d'utiliser des "étiquettes qu'on peut écrire sous la forme "ETIQUETTES:".

Avec les étiquettes, il suffit d'écrire JMP ETIQUETTES pour arriver directement à l'instruction qui suit le étiquette. Un petit exemple de code:

DEBUT :

MOV AX,14

JMP DEBUT

Ce programme ne fait rien sinon placer continuellement 14 dans AX, on appelle cela une boucle.

Nous verrons plus tard qu'il existe différents types de sauts.

Retour au sommaire

C - Quelques instructions arithmétiques : ADD et SUB

ADD sert à additionner, et nécessite deux opérandes : une source et une destination.

La destination prendra la valeur de source + destination. Les règles de combinaison sont les mêmes que pour MOV mais avec SUB et ADD, l'utilisation des registres de segment est impossible.

Exemples :

ADD AX,CX (si AX=10 et CX=20 alors nous aurons AX=30 et CX=20)
ADD EAX,EBX
ADD AX,-123 (l'assembleur autorise les nombres négatifs)
SUB DI,12

Quelques exemples impossibles :

ADD AX,CL (comme pour MOV, un 16 bits avec un 8 bits sont incompatibles)
SUB ES:[DI],DS:[SI]
ADD ES,13 (impossible car c'est un registre de segment)

Retour au sommaire

3 - Pile - Interruptions

A - La pile (Stack) et ses instructions

La pile (stack en anglais) est un emplacement où des données de petites tailles peuvent être placées. Cette mémoire temporaire (elle se comporte comme la mémoire vive mais en plus petit) est utilisée aussi dans les calculatrices et dans tous les ordinateurs. Le système qu'elle emploie pour stocker les données est du principe du "dernier stocké, premier à sortir" (LIFO -last in, first out). Cela veut dire que lorsqu'on place une valeur (un registre, un nombre) dans la pile, elle est placée au premier rang (placée en priorité par rapport aux autres valeurs dans la pile). Lors de la lecture de la pile pour récupérer la valeur, ce sera la première qui sera prise. La pile se comporte comme une pile d'assiettes à laver.

Au fur et à mesure que les assiettes sont sales, vous les empilez. L'assiette qui se trouve tout en bas sera la dernière à être lavée, tandis que la première assiette empilée sera lavée au tout début. Même chose en asm, sauf les assiettes vont finir dans des registres.

Avant que n'importe quel appel de fonction (API Windows par exemple), le programme doit pousser tous les paramêtres auxquels la fonction s'attend sur la pile. On utilise donc cette pile comme une mémoire d'échange des paramêtres entre fonction/programme, ou encore comme mémoire des variables locales des programmes/fonctions.

Prenons l'exemple suivant :

La fonction GetDlgItemText de l'API Windows exige les paramêtres suivants :

1 - handle of dialog box : l'handle de la boîte de dialogue.
2 - Identifier of control : identificateur du contrôle
3 - Address of buffer for text : adresse du tampon pour le texte
4 - Maximum size of string : format maximum pour la chaîne

Par conséquent ceux-ci ont pu être passés comme ainsi :

MOV EDI, [ESP+00000220] --------; place l'handle de la boite de dialogue dans EDI
PUSH 00000100 --------------------; push (4) taille maxi de la chaîne
PUSH 00406130 --------------------; push (3) adresse du buffer pour le texte
PUSH 00000405 --------------------; push (2) identificateur du contrôle
PUSH EDI --------------------------; push (1) hanlde de la boite de dialogue
CALL GetDlgItemText --------------; call la fonction

Facile hein ?? Ceci peut être une des manière les plus simples pour cracker un numéro de série, si vous savez l'adresse du tampon pour le numéro de série, dans ce cas-ci 00406130, poser juste un breakpoint (permettra d'arrêter le programme à cette adresse précise), et vous finirez habituellement vers le haut ou autour du procédé qui produit la vraie publication du serial !

Retour au sommaire

Les instructions de la pile

a ) PUSH.

Nous avons tout d'abord l'instruction PUSH qui signifie POUSSER. Cette instruction permet de placer une valeur au sommet de la pile. PUSH doit être accompagné d'une valeur de 16 ou 32 bits (souvent un registre) ou d'une valeur immédiate.

Exemple 1:

PUSH AX
PUSH BX

En premier lieu, AX est placé en haut de la pile et ensuite BX est placé au sommet, AX est repoussé au deuxième rang.

Exemple 2:

PUSH 1230
PUSH AX

Nous plaçons dans la pile, la valeur 1230 (qui aurait pu être aussi un 32 bits) et ensuite AX, ce qui place 1230 au deuxième rang.

Retour au sommaire

b ) POP.

Passons maintenant à l'instruction de "récupération" qui se nomme POP (sortir-tirer).Cette instruction demande comme PUSH, une valeur de 16 ou 32 bits (seulement un registre).Elle prend la première valeur de la pile et la place dans le registre qui suit l'instruction.

Exemple 1:

Nous avions PUSH AX et PUSH BX et maintenant nous allons les sortir de la pile en utilisant

POP BX
POP AX

Nous retrouvons les valeurs initiales de AX et BX mais nous aurions eu aussi la possibilité de faire d'abord POP AX et POP BX, ce qui aurait placé dans AX, la valeur BX (1er dans la pile) et dans BX, la valeur AX (2ème).

Exemple 2:

La première partie était PUSH 1230 et PUSH AX. Nous allons placer dans CX, la valeur de AX (1er de pile) et dans DX, le nombre 1230 (2ème puisque "pilé" après)

POP CX
POP DX

Dans CX, nous avons la valeur de AX et dans DX, nous avons 1230.

La pile est très utile pour garder la valeur d'un registre que l'on va utiliser afin de le retrouver intact un peu plus loin. Souvent dans des routines, le programmeur doit utiliser tout les registres mais leur nombre est limité (utilisation des registres au maximum = gain de vitesse). Le programmeur va donc utiliser les fonctions de la pile pour pouvoir utiliser les registres et garder leur valeur actuelle pour une utilisation ultérieure. Malheureusement les instructions de la pile ralentissent le programme, la pile étant de toute façon plus lente que les registres qui sont au coeur du CPU.

Retour au sommaire

B - Les interruptions - Instructions

Dans un ordinateur, il y a plusieurs périphériques (imprimante, souris, modem,...). Vous connaissez certainement le BIOS qui se charge chaque fois que vous allumez votre ordinateur.

Il y a différents BIOS qui dépendent des constructeurs mais ils sont généralement tous compatibles aux niveaux des interruptions. Le BIOS possède différentes fonctions, ce sont les interruptions. Ces interruptions sont divisées en 7 parties principales (il y en a d'autres):

- Fonctions vidéos (int 10h)
- Fonctions disques (int 13h)
- Fonctions de communication sur port série (int 14h)
- Fonctions système - Quasiment inutiles (int 15h)
- Fonctions clavier (int 16h)
- Fonctions imprimante (int 17h)
- Fonctions de gestion de l'horloge (int 1Ah)

Il n'y en a qu'une qui est importante, il s'agit de INT. Elle permet d'appeler une des 7 catégories d'interruptions. Pour pouvoir sélectionner une fonction, il faut configurer les registres avec certaines valeurs que vous trouverez dans la liste d'interruptions (une dernière fois, procurez-vous en une !!!). Souvent, il faut juste changer AX mais parfois, tout les registres sont modifiés. Ensuite on appelle INT accompagné du numéro de catégorie de la fonction désirée. Par exemple, pour rentrer en mode texte 80x25 caractères, il faut écrire ceci:

MOV AX,03h

INT 10h

Le nombre 03 qui se trouve dans AX, signifie que nous voulons le mode texte 80x25 (13h est le mode 320x200x256 couleurs). L'instruction INT 10h appelle la catégorie 10h (fonctions vidéos).

Et c'est tout. Les interruptions permettent de faire plus facilement certaines actions qui demanderaient une programmation plus compliquée.

Retour au sommaire

4 - Les flags - Les sauts conditionnels - CMP

A - Les flags - Les indicateurs

Les flags, "indicateurs", sont des bits qui donnent certaines informations. Ils sont regroupés dans le registre de flag. Ces flags sont modifiés en fonction de l'exécution des différentes instructions. Celles-ci changent la valeur des flags selon le résultat obtenu. Voici une liste des différents flags et leur utilité. Les bits marqués du 1 ou du 0 ne sont pas utilisés.

Bit 1 : CF
Bit 2 : 1
Bit 3 : PF
Bit 4 : 0
Bit 5 : AF
Bit 6 : 0
Bit 7 : ZF
Bit 8 : SF
Bit 9 : TF
Bit 10 : IF
Bit 11 : DF
Bit 12 : OF
Bit 13 : IOPL
Bit 14 : NT
Bit 15 : 0
Bit 16 : RF
Bit 17 : VM

Nous n'étudierons que les 12 premiers, ce sont les plus importants.

a ) CF

Retenue

Nous avons tout d'abord CF (Carry Flag). C'est le flag dit de retenue.

Dans les opérations mathématiques, il arrive que le résultat de l'opération soit codé sur un nombre supérieur de bits. Le bit en trop est placé dans CF. De nombreuses instructions modifient aussi CF. Par exemple, les instructions CLC et CMC, la première mettant à zéro CF et la deuxième qui inverse la valeur de CF. STC (set carry) met le flag à 1.

b ) PF

Parité

Il s'agit du Parity Flag. La valeur de ce flag est 1 si le nombre de bits d'une opérande (paramètre d'une instruction) est pair.

c ) AF

Retenue auxiliaire

AF est l'auxiliary carry qui ressemble à CF.

d ) ZF

Zéro

Il s'agit du Zero Flag qui est misà un lorsque un résultat est égal à 0. Souvent utilisé pour les diverses opérations, il est utile pour éviter des problèmes de divisions (je vous rappelle que diviser par zéro est impossible).

e ) SF

Signe

SF signifie Signe Flag. Simplement, sa valeur passe à 1 si nous avons un résultat signé (négatif ou positif).

f ) IF

Interruption

IF pour Interrupt Flag, enlève la possibilité au processeur de contrôler les interruptions.

Si IF=0, le processeur ne commande pas et si IF=1 alors c'est le contraire.

L'instruction STI provoque IF=1 et CLI met IF=0.

g ) DF

Direction

Le flag DF est le Direction Flag. C'est ce Flag qui donne l'indication sur la manière de déplacer les pointeurs (références) lors des instructions de chaînes (soit positiviment, soit négativement).

Deux instructions lui sont associées, il s'agit de CLD et STD.

h ) OF

Débordement

OF est l'Overflow Flag (indicateur de dépassement). Il permet de trouver et de corriger certaines erreurs produites par des instructions mathématiques. Très utile pour éviter les plantages. Si OF=1 alors nous avons affaire à un Overflow. Il existe une instruction qui s'occupe de ce Flag, c'est INTO qui déclenche l'exécution du code qui se trouve à l'adresse 0000:0010 (interruption 4).

C'en est tout pour les flags, vous comprendrez leur utilité au fur et à mesure de votre progression mais passons aux instructions conditionnelles qui leur sont directement associées.

Retour au sommaire

B - Les instructions conditionnelles

Nous abordons une partie qui est nécessaire lors de la création d'un programme. Souvent, le programme doit faire une action selon la valeur d'un résultat. Les instructions conditionnelles comme leur nom l'indique, sont des instructions qui font une action selon un résultat. Elles se basent sur les flags pour faire leur choix.

Vous vous souvenez de l'instruction JMP, il s'agissait d'un simple saut vers une autre partie du programme. D'autres instructions comme JMP font des sauts mais selon certains critères, on les appelle des sauts conditionnels. Voici la liste des ces instructions avec la valeur de l'indicateur nécessaire à l'exécution.

JB - JNAE - JC
Below - Not Above or Equal - Carry
CF = 1

JAE - JNB - JNC
Above or Equal - Not Below - Not Carry
CF=0

JE - JZ
Equal - Zero
ZF=1

JNE - JNZ
Not Equal - Not Zero
ZF=0

JO - JNO
Overflow - Not Overflow
OF=1 - OF=0

JP - JPE
Parity - Parity Even
PF=1

JNP - JPO
No Parity - Parity Odd
PF=0

JS - JNS
Signed - Not Signed
SF=1 - SF=0

JA - JNBE
Above - Not Below or Equal
CF=0 et ZF=0

JBE - JNA
Below or Equal - Not Above
CF=1 ou ZF=1

JG - JNLE
Greater - Not Less or Equal
ZF=0 et SF=OF

JGE - JNL
Greater or Equal - Not Less
SF=OF

JL - JNGE
Less - Not Greater or Equal
SF (signé)=OF

JLE - JNG
Less or Equal - Not Greater
ZF=1 ou SF (signé)=OF

Pour tester tout cela, nous avons besoin d'une instruction, c'est CMP qui nous aidera à le faire.

Retour au sommaire

C - L'instruction CMP

Cette instruction va servir à tester différentes valeurs et modifier les flags en fonction du résultat. CMP est un SUB qui ne change ni la source ni la destination, seulement les flags. Un CMP BX,CX sera comme un SUB BX,CX à l'exception près que BX ne sera pas modifié.

Si BX=CX alors BX-CX=0 donc le flag ZF sera égal à 1. Si nous voulons faire un saut avec "égal à" (JNE ou JZ qui demande ZF=1), nous avons ZF=1 et comme JZ demande que ZF=1 pour faire le saut, nous avons donc le saut. Souvenez-vous simplement que la plupart du temps, il s'agit de comparaisons du genre :

effectue le saut :

plus grand que (nombres non-signés) ---> JA

plus petit que (nombres non-signés) ---> JB

plus grand que (nombres signés) -------> JG

plus petit que (nombres signés) -------> JL

égal à (signé et non-signé) -----------> JE ou parfois JZ

il suffit de rajouter un 'n' après le 'j' pour avoir la même instruction mais exprimée de facon négative

ne saute pas si :

plus grand que (nombres non-signés) ---> JNA (jump if _not above_)

...et ainsi de suite.


Cours : L'Assembleur

L'assembleur

Introduction

Ce cours vous semblera sans doute long et pénible (ça l'est également pour moi pour l'écrire ;-) ) car ce n'est pas forcément facile de comprendre tout ça, mais efforcez vous à bien vous concentrer dessus car c'est important si vous voulez devenir un bon cracker. Attention, il ne s'agit pas non plus d'apprendre tout cela par coeur. Je vous conseille de prendre des notes et de les garder à côté de vous quand vous crackez ou sinon, pour les faignants, d'imprimer ce cours disponible au format imprimable sur le Guide PDF.

Dans ce cours vous allez apprendre les bases de l'assembleur. L'assembleur c'est le langage de programmation que vous voyez quand vous désassemblez un fichier dans WinDasm. C'est un langage assez proche du langage machine.

Sommaire :

1° Le processeur et les registres
 A) Les registres généraux ou de travail
 B) Les registres d'offset ou pointeur
C) Les registres de segment
D) Le registre Flag
 2° La Pile et ses instructions
A) Fonctionnement de la pile
 B) L'instruction PUSH
C) L'instruction POP
 3° Les sauts conditionnels et le CMP
A) Les sauts conditionnels
B) L'instruction CMP
 4° Les opérations mathématiques
A) Addition et Soustraction : ADD et SUB
 B) Multiplication : MUL / IMUL
 C) Division : DIV / IDIV
 D) Autres division et multiplication : SHR et SHL
E) L'opposé d'un nombre : NEG
5° Les nombres à virgule et les nombres négatifs
A) Les nombres à virgule
 B) Les nombres négatifs
6 ° Les instructions logiques
A) ET logique : AND
B) OU logique inclusif : OR
 C) OU logique exclusif : XOR
 D) Non logique : NOT
 E) TEST
7° La mémoire et ses instructions
A) LEA
 B) MOVSx
 C) STOSx
 8° Fin du cours !

1° Le processeur et les registres

Les données dont le processeur a besoin sont stockées dans des "cases" qu'on appelle des registres. Il existe plusieurs types de registres et chacun a son utilité.

  • - registres généraux ou de travail
    Ils servent à manipuler des données, à transférer des paramètres lors de l’appel de fonction DOS et à stocker des résultats intermédiaires.

  • - registres d’offset ou pointeur
    Ils contiennent une valeur représentant un offset à combiner avec une adresse de segment

  • - registres de segment
    Ils sont utilisés pour stocker l’adresse de début d’un segment. Il peut s’agir de l’adresse du début des instructions du programme, du début des données ou du début de la pile.

  • - registre de flag
    Il contient des bits qui ont chacun un rôle indicateur.

A) Les registres généraux ou de travail

Les 4 registres généraux les plus utilisés sont : AX , BX , CX et DX.

  • AX -- accumulateur -- sert à effectuer des calculs arithmétiques ou à envoyer un paramètre à une interruption

  • BX -- registre auxiliaire de base -- sert à effectuer des calculs arithmétiques ou bien des calculs sur les adresses

  • CX -- registre auxiliaire (compteur) -- sert généralement comme compteur dans des boucles

  • DX -- registre auxiliaire de données -- sert à stocker des données destinées à des fonctions

Ceci est leur utilisation théorique, mais dans la pratique ils peuvent être utilisés à d'autres usages.

Ces registres sont de 16 bits (petit rappel : 8 bits = 1 octet et 1 bit = 0 ou 1) et chacun de ses registres est divisé en 2 parties : L (comme Low pour désigner les bits de poids faible) et H (pour High afin de désigner les bits de poids forts). Par exemple : AX (AL et AH), BX (BL et BH), CX (CL et CH), DX (DL et DH).
Pour obtenir un registre de 32 bits il faut rajouter un "E" (pour Extended, en français "étendu") devant le registre. Par exemple EAX (32 bits) pour AX (16 bits), (EBX pour BX, ECX pour CX, EDX pour DX). Il existe également des EAH ou EAL, mais la partie haute d'un registre 32 bits ne sera pas directement accessible.

Les bits correspondent en fait à leur capacité : 8 bits = nombre entre 0 et 255 (en décimal) au maximum, 16 bits = 65535 au maximum, 32 bits = 4 294 967 295 au maximum.

EAX (32 bits)
 | AX (16 bits)
 | AH (8 bits) | AL (8 bits)

B) Les registres d'offset ou pointeur

Voici les différents registres :

  • IP -- Pointeur d'instruction -- est associé au registre de segment CS (CS :IP) pour indiquer la prochaine instruction à exécuter. Ce registre ne pourra jamais être modifié directement ; il sera modifié indirectement par les instructions de saut, par les sous-programmes et par les interruptions

  • SI -- Index de source -- est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est associé au registre de segment DS.

  • DI -- Index de destination -- est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est normalement associé au registre de segment DS ; dans le cas de manipulation de chaînes de caractères, il sera associé à ES
  • SP -- Pointeur de pile -- est associé au registre de segment SS (SS :SP) pour indiquer le dernier élément de la pile.
  • BP -- Pointeur de base -- est associé au registre de segment SS (SS :BP) pour accéder aux données de la pile lors d’appels de sous-programmes (CALL).

Comme les registres généraux, ces registres peuvent être étendues à 32 bits en ajoutant le "E" : (EIP, ESI, EDI, ESP, EBP). Par contre ils ne possèdent pas de partie 8 bits (il n' y aura pas de EIH ou ESL par exemple).

Les registres SI et DI sont le plus souvent employés pour les instructions de chaîne et pour accéder à des blocs en mémoire.

Ces registres (sauf IP) peuvent apparaître comme des opérandes dans toutes les opérations arithmétiques et logiques sur 16 bits

ESI (32 bits)

SI (16 bits)

C) Les registres de segment

Le processeur utilise des adresses pour stocker ou récupérer des données. L'adresse est composée de 2 parties de 32 bits (ou 16 bits selon le mode utilisé). La première est le segment et la deuxième est l'offset. L'adresse est présenté comme cela : segment:offset (par exemple : 0A000h:00000h). Voici les différents segments :

  • CS -- Registre segment de code -- indique l’adresse du début des instructions d’un programme ou d’une sous-routine

  • DS -- Registre segment de données -- contient l’adresse du début des données de vos programmes. Si votre programme utilise plusieurs segments de données, cette valeur devra être modifié durant son exécution

  • SS -- Registre segment de pile -- pointe sur une zone appelée la pile

  • ES -- Registre segment supplémentaire (Extra segment) -- est utilisé, par défaut, par certaines instructions de copie de bloc. En dehors de ces instructions, le programmeur est libre de l’utiliser comme il l’entend

  • FS -- Registre segment supplémentaire -- Rôle semblable à ES

  • GS -- Registre segment supplémentaire -- Rôle semblable à ES

D) Le registre Flag

Ce registre est un ensemble de 16 bits. Mais cet ensemble ne signifie rien, car les bits sont modifiés un par un. Et la modification d'un seul bit peut changer le comportement du programme. Les bits de cet ensemble sont appelés "indicateurs".

Bit 1 : CF
Bit 2 : 1
Bit 3 : PF
Bit 4 : 0
Bit 5 : AF
Bit 6 : 0
Bit 7 : ZF
Bit 8 : SF
Bit 9 : TF
Bit 10 : IF
Bit 11 : DF
Bit 12 : OF
Bit 13 : IOPL
Bit 14 : NT
Bit 15 : 0
Bit 16 : RF
Bit 17 : VM

Les bits 1, 5, 12, 13, 14, 15 de ce registre Flag de 16 bits ne sont pas utilisés.

Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs.
Les instructions conditionnelles testent la valeur des indicateurs et agissent en fonction du résultat.

Voici à quoi les principaux indicateurs servent :

CF : Carry Flag - Indicateur de retenue

A la suite d'une opération, si le résultat est codé sur un nombre supérieur de bits, le bit de trop sera mis dans CF. Plusieurs instructions peuvent modifier son état : CLC pour le mettre à 0, STC à 1 et CMC inverse sa valeur.

PF : Parity Flag - Indicateur de parité

Egal à 1 si le nombre de bits d'une opérande (paramètre d'une instruction) est pair.

AF : Auxiliary carry Flag - Indicateur de retenue auxiliaire

Cet indicateur ressemble à CF.

ZF : Zero Flag - Indicateur zéro

Si le résultat d'une opération est nul (égal à 0) ZF passera à 1. Cet indicateur est souvent utilisé pour savoir si deux valeurs sont égales (en les soustrayant).

SF : Sign Flag - Indicateur de signe

SF passe à 1 quand le résultat est signé (négatif ou positif).

IF : Interruption Flag - Indicateur d'interruption

Il permet d'enlever la possibilité au processeur de contrôler les interruptions. Si IF = 1 les interruptions sont prises en compte et si IF = 0 elles sont ignorées. L'instruction STI met IF à 1 et CLI le met à 0.

DF : Direction Flag - Indicateur de direction

C'est ce Flag qui donne l'indication sur la manière de déplacer les pointeurs (références) lors des instructions de chaînes (soit positivement, soit négativement). STD le met à 1 et CLD à 0.

OF : Overflow Flag - Indicateur de dépassement

Il permet de trouver et de corriger certaines erreurs produites par des instructions mathématiques. Très utile pour éviter les plantages. Si OF=1 alors nous avons affaire à un Overflow.

Les autres indicateurs ne sont pas importants nous n'allons donc pas nous attarder dessus.

Petit exemple sur ces indicateurs avec une opération en binaire :

0 1 1 0
+ 0 1 0 1
--------
1 0 1 1
  • ZF=0 (le résultat 1011 n'est pas nul)
  • SF=1 (le signe du résultat est négatif)
  • CF=0 (il n'y a pas de retenue)

2° La Pile et ses instructions

A) Fonctionnement de la pile

La pile ("stack" en anglais) est un emplacement qui sert à stocker de petites données. Cette mémoire temporaire fonctionne comme une pile d'assiette : c'est-à-dire que la dernière assiette que vous empilez sera la première à être prise et la première assiette empilée (celle qui est tout en dessous) sera la dernière prise. J'espère avoir été clair dans cette métaphore. Pour prendre un vrai exemple, si je mets la valeur "A" dans la pile puis je mets "B" et après "C". Pour récupérer ces valeurs le processeur va d'abord prendre "C", puis "B" et ensuite "A". Là si vous ne comprenez toujours pas je ne peux plus rien pour vous !

Pour l'API GetDlgItemText, Windows a besoin de ces informations :

HWND hDlg, // (1) handle de la boîte de dialogue
int nIDDlgItem, // (2) identifiant de la boîte de dialogue
LPTSTR lpString, // (3) pointe vers le buffer pour le texte
int nMaxCount // (4) taille maximum de la chaîne

On peut donc mettre avant d'appeler cette fonction :

MOV EDI, [ESP+00000220] place l'handle de la boite de dialogue dans EDI
PUSH 00000100 push (4) taille maxi de la chaîne
PUSH 00406130 push (3) adresse du buffer pour le texte
PUSH 00000405 push (2) identificateur du contrôle
PUSH EDI push (1) hanlde de la boite de dialogue
CALL GetDlgItemText appelle la fonction

B) L'instruction PUSH

Vous avez pu voir cette instruction au dessus. Push signifie "pousser" en français. C'est avec cette instruction qu'on place une valeur sur la pile. La valeur "poussé" doit être de 16 ou 32 bits (souvent un registre) ou une valeur immédiate.

Par exemple :
PUSH AX
PUSH BX
PUSH 560
AX est placé en haut de la pile en premier mais ce sera 560 qui en ressortira en premier.

C) L'instruction POP

Mettre des données sur la pile c'est bien, mais le but c'est de les récupérer par la suite. C'est donc à ça que sert l'instruction POP : récupérer les données placées sur la pile.

Par exemple (suite à l'exemple au dessus) :
POP CX
POP BX
POP AX
Ces instructions mettront la première instruction du haut de la pile dans CX, c'est à dire 560. Et ensuite BX reviendra dans BX et AX dans AX.

La pile est très utilisée pour retrouver les valeurs intactes des registres. Car le nombre de registres est limité, donc on utilise la pile lorsque l'on n'a plus de place. Mais la pile est plus lente que les registres.

3° L es sauts conditionnels et le CMP

A) Les sauts conditionnels

Dans les différents sauts il y a les inconditionnels (qui sautent sans conditions) comme le JMP (Jump) et ceux qui sont conditionnels (qui sautent donc si la condition est vérifiée). Dans cette dernière "famille" il y a du monde. ;-) Les valeurs des indicateurs sont nécessaires pour la vérification de la condition.

Voici les principaux sauts, sachez que J = Jump if (Saut si) :

Indicateur | Valeur | Saut | Signification
CF | 1 | JB | Below - Inférieur
 |  | JBE | Below or Equal - Inférieur ou égal
 |  | JC | Carry - Retenue
 |  | JNAE | Not Above or Equal - Non supérieur ou égale
 | 0 | JA | Above - Supérieur
 |  | JAE | Above or Equal - Supérieur ou égal
 |  | JNB | Not Below - Non inférieur
 |  | JNC | Not Carry - Pas de retenue
ZF | 1 | JE | Equal - Egal
 |  | JNA | Not Above - Non supérieur
 |  | JZ | Zero - Zero (Saut en cas d'égalité)
 | 0 | JNBE | Not Below or Equal - Non supérieur ou égal
 |  | JNE | Not Equal - Non égal
 |  | JNZ | Not Zero - Non zéro (saut si différent)
PF | 1 | JP | Parity - Pair
 |  | JPE | Parity Even - Pair
 | 0 | JNP | Not Parity - Non pair
 |  | JPO | Parity Odd - Non pair
OF | 1 | JO | Overflow - Dépassement
 | 0 | JNO | Not Overflow - Pas de dépassement
SF | 1 | JS | Signed - Signé
 | 0 | JNS | Not Signed - Non signé

Et quelques autres :

JG - JNLE
Greater (arithmétiquement supérieur) - Not Less or Equal (arithmétiquement non inférieur ou égale)
ZF=0 et SF=OF

JGE - JNL
Greater or Equal (arithmétiquement supérieur ou égal) - Not Less (non inférieur arithmétiquement)
SF=OF

JL - JNGE
Less (arithmétiquement inférieur) - Not Greater or Equal (arithmétiquement non supérieur ou égal)
SF (signé)=OF

JLE - JNG
Less or Equal (arithmétiquement inférieur ou égal) - Not Greater (non supérieur arithmétiquement)
ZF=1 ou SF (signé)=OF

B) L'instruction CMP

Cette instruction permet d'effectuer des tests entre des valeurs et de modifier les flags suivant le résultat. Le fonctionnement du CMP est semblable au SUB (que l'on verra plus loin et qui sert pour la soustraction). Il s'utilise de cette façon : CMP AX, BX. Cela permet de vérifier si ces 2 valeurs sont égales car il fait AX-BX, si c'est égal à 0 alors ZF=1 sinon ZF=0.

Le plus souvent il est suivi de ces sauts et leur inverse (en rajoutant un "N" après le "J", par exemple JNE pour JE...) :

JA : plus grand que (nombres non-signés)
JB : plus petit que (nombres non-signés)
JG : plus grand que (nombres signés)
JL : plus petit que (nombres signés)
JE ou JZ : égal à (signé et non-signé)

4° L es opérations mathématiques

A) Addition et Soustraction : ADD et SUB

Ces 2 instructions nécessitent 2 opérandes : la source et la destination. Ce qui donnera : Destination = Source + Destination. Et en assembleur : ADD Destination, Source.
Exemple avec AX = 4 et BX = 6 :

ADD AX, BX

Après cette opération : AX = 4+6 = 10 et BX = 6

Le fonctionnement de SUB est identique.

Il est impossible d'ajouter ou soustraire un 16 bits avec un 8 bits par exemple. Donc ADD BX, CL sera impossible !

B) Multiplication : MUL / IMUL

L'instruction MUL est utilisé pour les nombres non signés et IMUL pour les signés (petit rappel : les nombres signés peuvent être négatifs contrairement aux non signés, par exemple en 16 bits en non signé les nombres vont de 0 à 65535 et en signé ils vont de -32768 à 32767). Contrairement aux opérations précédentes, la multiplication n'a besoin que d'une seule opérande : la source. La destination et donc le nombre qui devra être multiplié se trouvera toujours et obligatoirement dans AX. La source est un registre.

Exemple avec AX = 4 et BX = 6

MUL BX

Résultats : AX = 4*6 = 24 et BX = 6

IMUL fonctionne de la même façon, sauf qu'il faut utiliser l'instruction CWD (convert word to doubleword), qui va permettre "d'étendre" le signe de AX dans DX. Si cela n'est pas fait les résultats pourront être erronés.

C) Division : DIV / IDIV

Pareil que MUL et IMUL : DIV sert pour les nombres non signés et IDIV pour les non signés. Cette instruction divise la valeur de AX par la source.

Exemple avec AX = 18 et BX = 5 :

IDIV BX

Résultats : AX = 3 et DX = 3 (DX étant le reste)

D) Autre division et multiplication : SHR et SHL

Ces instructions permettent de diviser des registres avec SHR et les multiplier avec SHL. Ils demandent 2 opérandes : le nombre à diviser et le diviseur pour SHR et le nombre à multiplier et le multiplicateur pour SHL. Ils fonctionnent en puissance de 2. Donc si on veut diviser AX par 4 on mettra : SHR AX, 2 (car 2^2 = 4). Pour diviser BX par 16 on mettra : SHR BX,4 (car 2^4 = 16). Pour SHL c'est pareil : pour multiplier CX par 256 on mettra donc : SHL CX,8 (2^8 = 256). Pour multiplier ou diviser des nombres qui ne sont pas des puissances de 2 il faudra combiner des SHR ou SHL.
Petit rappel de mathématiques : x^0 donnera toujours 1 quelque soit x !

Exemple : on veut multiplier 5 par 384. On voit que 384 = 256 + 128, ce qui nous arrange car 256 et 128 sont des puissances de 2.

  • MOV AX,5 >> On met 5 dans AX >> AX = 5

  • MOV BX,AX >> On met AX dans BX >> BX = AX

  • SHL AX,8 >> On multiplie AX par 256 (2^8) >> AX = AX*256 = 5*256 = 1280

  • SHL BX,7 >> On multiplie BX par 128 (2^7) >> BX = BX*128 = 5*128 = 640

  • ADD AX,BX >> On ajoute BX à AX >> AX = AX + BX = 1280 + 640 = 1920

Et vous pouvez vérifier : 5*384 = 1920 ! ;-)

Mais vous me direz : "pourquoi ne pas faire simplement un MUL ?". Et bien car SHL est beaucoup plus rapide à exécuter. Mais pour des nombres qui ont une décomposition trop longue, mieux vaut bien sur utiliser un MUL.

E) L'opposé d'un nombre : NEG

L'instruction la plus simple à mon avis : NEG sert à rendre un nombre positif en négatif et inversement. Il ne demande qu'une seule opérande : la destination. Si AX = 5, alors un NEG AX mettra AX = -5. Ou si BX = -14 alors après le NEG BX, BX = 14.

5° L es nombres à virgule et les nombres négatifs

A) Les nombres à virgule

Le problème en assembleur c'est qu'on ne peut pas utiliser directement les nombres à virgule. Nous utiliserons les nombres à virgule fixe. Il s'agit simplement d'effectuer des calculs avec des valeurs assez grandes qui seront ensuite, redivisées pour retrouver un résultat dans un certain intervalle.

Par exemple nous voulons multiplier 20 par 0,25. Comme on ne peut pas mettre 0,25 dans un registre on va le mettre en nombre entier en le multipliant par 256 par exemple. 0.25*256 = 64. On le multipliera à 20 et ensuite ce résultat étant 256 fois trop grand on le redivisera par 256 :

  • MOV AX,64 >> On place 0,25*256 dans AX >> AX = 64

  • MOV BX,20 >> On place 20 dans BX >> BX = 20

  • MUL BX >> On multiplie AX par BX >> AX = AX*BX = 64*20 = 1280

  • SHR AX,8 >> On divise AX par 256 (2^8) >> AX = AX/256 = 1280/256 = 5

Le résultat de 20*0,25 est donc bien égal à 5.

Faîtes bien attention à ne pas effectuer de dépassement, surtout avec les nombres signés ou les grands nombres. En utilisant SHR ou SHL, plus l'opérande est grande et plus le résultat sera précis, car il y aura plus de décimales disponibles.

B) Les nombres négatifs

Un autre problème, c'est qu'en décimal pour un nombre négatif on place un "-" devant le nombre, mais en binaire on ne peut mettre que des 0 ou des 1. Pour pouvoir mettre un nombre en négatif on utilise la méthode du complément à 2. Voici en quoi cela consiste :

  • Convertir le nombre en binaire s'il ne l'est pas déjà

  • Inverser les bits du nombre (inverser les 0 en 1 et 1 en 0)

  • Ajouter 1 au nombre obtenu

Voici un exemple avec le nombre décimal 5 au format 8 bits (octet) :

  • On le convertit en binaire : 5(d) = 00000101(b)

  • On inverse : 00000101 --> 11111010 (ici le format, 8 bits, est important car en 4 bits par exemple cela donnera : 1010)

  • On ajoute 1 : 11111010 + 00000001 = 11111011

-5 est donc égal à 11111011 en binaire.

Pour que le nombre soit négatif il faut qu'il soit signé. En 8 bits en non signé les nombres vont de 0 à 255 et en signé ils vont de -128 à 127.

Quand le nombre est signé et en 8 bits, on peut savoir s'il est négatif en regardant le 8ème bit (en partant bien sur de la droite). S'il est égal à 1 alors le nombre est négatif. Pour les nombres à 4 bits c'est le 4ème qui compte.

Pour transformer -5 en hexadécimal, on décompose le nombre binaire en demi-octet (11111011 en 1111 et 1011). 1111b = 15d = Fh et 1011b = 11d = Ah. Donc -5d = FAh. (Petit rappel : d = décimal, b = binaire, h = hexadécimal, pour ceux qui n'auraient pas encore compris).

6° L es instructions logiques

Les opérations de ces instructions se font bit à bit. Et les opérandes et emplacements mémoires qu'elles utilisent sont en 8 ou 16 bits.

Petit rappel : pour convertir de tête un nombre binaire en décimal : chaque rang d'un nombre binaire est un multiple de 2.

2^7 | 2^6 | 2^5 | 2^4 | 2^3 | 2^2 | 2^1 | 2^0
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1

Ainsi par exemple 10110110 donnera 128+32+16+4+2 = 182 :

1 | 0 | 1 | 1 | 0 | 1 | 1 | 0
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1

A) Et logique : AND

AND (ET) effectue un ET LOGIQUE sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le ET LOGIQUE donne le résultat 1 uniquement quand les 2 opérandes sont 1, sinon il met 0.

opérande 1 | opérande 2 | AND
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

Exemple avec AX = 15 et BX = 26 :

AND AX,BX

Résultat :

0000 1111 (AX=15)
AND 0001 1010 (BX=26)
-------------
0000 1010 (AX = 10)

B) Ou logique inclusif : OR

OR effectue un OU LOGIQUE INCLUSIF sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le OU LOGIQUE INCLUSIF donne le résultat 0 uniquement quand les 2 opérandes sont 0, sinon il met 1.

opérande 1 | opérande 2 | OR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1

Exemple avec AX = 15 et BX = 26 :

OR AX,BX

Résultat :

0000 1111 (AX=15)
OR 0001 1010 (BX=26)
-------------
0001 1111 (AX = 31)

C) Ou logique exclusif : XOR

XOR effectue un OU LOGIQUE EXCLUSIF sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le OU LOGIQUE EXCLUSIF donne le résultat 1 quand les 2 opérandes sont différentes, sinon il met 0.

opérande 1 | opérande 2 | XOR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

Exemple avec AX = 15 et BX = 26 :

XOR AX,BX

Résultat :

0000 1111 (AX=15)
XOR 0001 1010 (BX=26)
-------------
0001 0101 (AX = 21)

D) Non logique : NOT

NOT effectue un NON LOGIQUE sur une opérande et le résultat se retrouvera dans l'opérande. Le NON LOGIQUE inverse la valeur de chaque bit.

opérande | NOT
0 | 1
1 | 0

Exemple avec AX = 15 :

NOT AX

Résultat :

NOT 0000 1111 (AX=15)
-------------
1111 0000 (AX = -16 en signé et 240 en non signé)

E) TEST

Cette instruction teste la valeur d’un ou plusieurs bits en effectuant un ET LOGIQUE sur les opérandes. Le résultat ne modifie pas les opérandes mais les indicateurs.

Exemple :

TEST AX,1 >> Effectue un ET LOGIQUE sur le premier bit de AX avec 1, si il est égal à 1, ZF passera à 1 et inversement (si 0 alors ZF=0).

Un TEST AX,AX (souvent utilisé) permet de voir si AX est égal à 0 ou non.

7° La mémoire et ses instructions

A) LEA

Pour placer par exemple la valeur de l'offset de MESSAGE dans SI il suffit de mettre : MOV SI,Offset-Message. Mais on ne peut pas procéder de cette façon pour les registres DS, ES, FS, GS car ceux ci n'acceptent pas les valeurs numériques, ils n'acceptent que les registres. Il faudra donc passer par : MOV AX,Seg-Message puis MOV DS,AX.

Il existe une autre instruction, LEA, qui permet de placer l'offset dans un registre mais en plus court. Par exemple : LEA SI,MESSAGE placera dans SI l'offset de Message. L'utilisation de LEA à la place de MOV rend le code plus clair et plus compact.

B) MOVSx

Pour déplacer des blocs entiers de mémoires il faut utiliser les instructions : MOVSB, MOVSW ou MOVSD selon le nombre de bits à déplacer.

  • MOVSB : (B) = Byte (Octet : 8 bits) (Attention !! En anglais "byte"= un octet et "bit"= un bit)
  • MOVSW : (W) = Word (Mot : 16 bits)
  • MOVSD : (D) = DWord (Double Mot : 32 bits)

Si on veut déplacer 1000 octets (bytes) en utilisant la commande MOVSB il faudra qu'elle se répète 1000 fois. Donc l'instruction REP est utilisée comme une boucle. Il faut placer auparavant le nombre de boucles à effectuer dans le registre de compteur (CX) :

MOV CX,1000 >> Nombre de boucles à effectuer dans le compteur CX
REP MOVSB >> Déplace un octet

Pour aller plus vite on peut les déplacer par Mot (Word) :

MOV CX,500 >> 1000 bytes = 500 words
REP MOVSW >> Déplace un mot

Ou alors en Double Mot (DWord) :

MOV CX,250 >> 1000 bytes = 500 words = 250 dwords
REP MOVSD >> Déplace un Double Mot

A chaque MOVSx, DI augmente de 1,2 ou 4 bytes

C) STOSx

Pour garder des données ou les placer dans un emplacement de la mémoire on utilise les instructions STOSB, STOSW ou STOSD.

Pareil que les MOVSx, ces instructions servent respectivement à stocker un byte, un word et un dword. Par contre ils utilisent AL, AX (EAX 32bits) comme valeur à stocker. La valeur de AL,AX,EAX sera stockée dans ES:DI. A chaque STOSx, DI est augmenté de 1,2 ou 4 bytes.

Exemple :

MOV CX,10 >> nombre de répétitions placées dans CX, ici 10
MOV AX,0 >> valeur à déplacer, ici 0
REP STOSB >> stocker AX dans ES:DI

Nous aurons donc 10 bytes de 0 dans ES:DI.

8° Fin du cours !

Voilà c'est la fin du cours ! "Ouf" diront certains, "Oh quel dommage" diront les hypocrites ! ;-) Je félicite ceux qui ont réussi à lire tout ça ! ;-) J'espère que cela vous servira à mieux comprendre en crackant. Avec ça vous devriez vous sentir plus à l'aise (enfin si vous avez compris mon cours).

J'espère avoir été le plus compréhensible possible. Mais si vous ne comprenez pas quelque chose à propos de ce cours demandez sur le forum plutôt que de m'envoyer un mail sur deamon.crack (at) netcourrier.com car j'essaye de toute façon de lire tous les posts du forum et d'autres personnes que moi pourront vous aider.

Je voulais féliciter le très bon cours de Falcon sur l'assembleur qui m'a permis de découvrir l'assembleur. D'ailleurs j'ai repris un peu son plan, car je n'ai pas réussi à en faire un bien, et j'ai dû copier seulement 2-3 phrases. Mais c'est bien moi qui ai écrit tout ça, en 6-8 heures (et oui c'est très long à écrire tout ça) !

Pendant que je suis là ;-) , je remercie également tout ceux qui viennent sur mon site, ceux qui m'envoient des mails de sympathie et ceux qui participent plus ou moins activement sur le forum.

Deamon, le 15-16 juillet 2003


Les bases de l'asm

Vu dans .... http://deamonftp.free.fr/deamoncrack/Tuts/Crisanar/Baseasm.htm

Introduction

Quoi de plus ardu que d'essayer de résumer l'asm pour n'en garder que l'essentiel. De plus, comment voulez-vous posséder un bon niveau de programmeur mais aussi d'analyste si des instructions vous font défaut? D'un autre côté, rien ne sert de se leurrer, peu sont ceux qui peuvent se vanter de connaître toutes les instructions sur le bout des doigts ainsi que toutes les astuces qui en découlent. C'est pourquoi je ne perdrais pas ici mon temps à détailler toutes les instructions ni à faire du plagia (en cherchant un peu, Google regorge de tutos plus ou moins bien expliqués et complets sur l'asm). Je vais simplement présenter brièvement l'asm et faire un bref rappel de quelques instructions qui me semblent intéressantes pour le cracking mais sans rentrer dans les détails techniques, ainsi que de quelques astuces utilisées avec celles-ci. Libre à vous de continuer votre apprentissage sur d'autres sites.

Les registres

Les registres principaux sont : EAX, EBX,ECX, EDX, EDI, ESI, EBP, ESP, EIP et les Flags.

Ce sont des petites zones de stockage logées dans le processeur auxquels il a un accès très rapide (beaucoup plus rapide qu'en mémoire).

Ils ont tous une taille de 32 bits (4 bytes).

EAX, EBX, ECX et EDX sont les registres de travail. La partie basse (les 16bits de poids fort) de chacun d'eux peut être utilisée directement mais reste une partie du registre de 32bits, c'est à dire que s'il l'on modifie AX (la partie basse de EAX), on modifiera EAX. En plus de cela, la partie basse peut être de nouveau séparée en 2 parties de 8bits (la partie haute AH et la partie basse AL pour AX, BH et BL pour BX, ...). De nouveau si l'on modifie l'une des 2 parties, c'est tout le registre qui est modifié (AX est modifié donc EAX aussi).

EDI et ESI servent principalement pour des instructions d'adressage (lors de traitements de chaînes de caractères par exemple) mais peuvent aussi servir de registres de travail.

EBP et ESP sont des registres de pile, ESP désigne toujours le sommet de la pile. EBP permet de travailler dans la pile sans devoir utiliser ESP qu'il ne vaut mieux pas modifier.

EIP désigne l'adresse de l'instruction courante.

Le registre de Flags est un registre dont chaque bit (indicateur) indique un état. Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs.

La pile

La pile est un espace mémoire régit par le système LIFO (Last In First Out). La dernière valeur à avoir été empilée sera la première à être dépilée. Imaginez une pile d'assiettes : la dernière que vous avez déposée sur le paquet sera celle que vous reprendrez en première, c'est le principe de la pile. On y accède par des PUSH et des POP

Quelques instructions courantes

NOP (90h)

La plus simple à expliquer : cette instruction ne fait rien! Vous le savez peut-être mais nous ne pouvons modifier la taille d'un programme sans des conséquences désastreuses. C'est là que cette instruction prend toute son utilité. Elle permet de supprimer une instructions sans modifier la taille du programme (si l'instruction à "effacer" tient sur plusieurs bytes remplacez autant de bytes que nécessaires par NOP).

MOV destination, source

Cette instruction copie la valeur de source dans destination. Les 2 opérandes doivent avoir la même taille. Il n'y peut y avoir au maximum qu'un accès mémoire par instruction. Si vous voulez par exemple mettre la valeur de [00401234] dans [00405678], vous devrez faire par exemple:
MOV EBX , [401234]

MOV [405678] , EBX

mov eax , 45 ; Mettre 45 dans eax

ADD destination, source

Cette instruction additionne le contenu de destination à celui de source. Le résultat est placé dans destination.

mov eax,45

add eax,10 ; eax <= eax (45) + 10 donc eax vaudra 55

SUB destination, source

Cette instruction soustrait le contenu de source à celui de destination. Le résultat est placé dans destination.

mov eax,45

sub eax,5 ; eax <= eax (45) - 5 donc eax vaudra 40

INC destination

Cette instruction incrémente destination d'une unité

mov eax,10

inc eax ; eax <= eax + 1

DEC destination

Cette instruction décrémente destination d'une unité

mov eax,10

dec eax ; eax <= eax - 1

JMP label

Modifie l'EIP en l'adresse désignée par label. Ce que l'on désigne par un saut.

jmp label

; instructions non exécutées

label:

; instructions à éxécuter

PUSH source

Place la valeur de source au sommet de la pile.

POP destination

Place la valeur au sommet de la pile dans destination.

mov eax,15

push eax ; Place la valeur de eax (15) sur la pile

pop ebx ; Récupère la valeur au sommet de la pile (15, c'est celle que l'on vient de mettre) et la place dans ebx

CMP destination, source

Cette instruction effectue la soustraction de destination et source mais ne conserve pas le résultat. Seuls les Flags sont modifiés.

Le ZF (Zero Flag) est positionné à 1 si le résultat de la soustraction est nul (donc que les 2 valeurs sont égales).

Le CF (Carry Flag) est positionné à 1 si le résultat ne tient pas sur 32bits (donc que le résultat est est inférieur à 0).

Le SF (Sign Flag) est positionné à 1 si le résultat est négatif.

Le OF (Overflow Flag) est positionné à 1 si le résultat a provoqué un débordement (lorsqu'on travaille avec des nombres signés).

mov eax,5

cmp eax,10 ; eax < à 10 => ZF=0, SF=1 et CF=1

cmp eax,2 ; eax > à 2 => ZF=0, SF=0 et CF=0

cmp eax,5 ; eax = 5 => ZF=1, SF=0 et CF=0

Les sauts conditionnels

Fonctionne comme un JMP mais saute en fonction de l'état de certains bits du registre de Flags.

Les différents sauts sont :

Mnémo | Saut si … | Indicat. testés
JA | Supérieur | CF = 0 et ZF = 0
JAE | Supérieur ou égal | CF = 0
JB | Inférieur | CF = 1
JBE | Inférieur ou égal | CF = 1 ou ZF = 1
JC | CF = 1 | CF = 1
JCXZ | Registre CX à 0 | CX = 0
JE | Egal | ZF = 1
JG* | Supérieur | ZF = 0 et SF = OF
JGE* | Supérieur ou égal | SF = OF
JL* | Inférieur | SF<>OF
JLE* | Inférieur ou égal | ZF = 1 ou SF<>OF
JNA | Pas supérieur | CF = 1 ou ZF = 1
JNAE | Pas supérieur ni égal | CF = 1
JNB | Pas inférieur | CF = 0
JNBE | Pas inférieur ni égal | CF = 0 et ZF = 0
JNC | CF = 0 | CF = 0
JNE | Pas égal | ZF = 0
JNG* | Pas supérieur | ZF = 1 et SF<>OF
JNGE* | Pas supérieur ni égal | SF<>OF
JNL* | Pas inférieur | SF = OF
JNLE* | Pas inférieur ni égal | ZF = 0 et SF = OF
JNO | OF = 0 | OF = 0
JNP | PF=0 | PF = 0
JNS* | SF=0 | SF = 0
JNZ | ZF=0 | ZF = 0
JO* | OF=1 | OF = 1
JP | PF=1 | PF = 1
JPE | Parité paire | PF = 1
JPO | Parité impaire | PF = 0

* Utiliser avec des nombres signés

mov eax,2

cmp eax,5

je lblfin ; On ne saute pas puisque 2<>5

; Instructions qui vont être exécutées puisque eax <> 5 et donc on ne sautera pas

lblfin:

Les opérations logiques

Les opérations logiques travaillent toutes bit à bit. Elle peuvent servir à comparer deux valeurs, tester certains bits mais aussi à forcer certains bits à une certaine valeur.

AND destination, source

Le AND effectue un ET LOGIQUE de destination et source. Le AND peut se traduire par une multiplication de bits.

Voici la table logique :

A | B | AND
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

mov al,14h ; 00010100b

mov bl,2Fh ; 00101111b

and al,bl ; 00000100b = 04h => al va prendre la valeur 04h

OR destination, source

Le OR effectue un OU inclusif de destination et source. Le OR peut se traduire par une addition de bits sans report.

A | B | OR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1

mov al,14h ; 00010100b

mov bl,2Fh ; 00101111b

or al,bl ; 00111111b = 3Fh => al va prendre la valeur 3Fh

XOR destination, source

Le XOR effectue un OU inclusif de destination et source. Le XOR peut se traduire par une addition de bits avec report.

A | B | XOR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

mov al,14h ; 00010100b

mov bl,2Fh ; 00101111b

xor al,bl ; 00111011b = 3Bh => al va prendre la valeur 3Bh

Voilà, ce n'est évidemment pas complet mais j'espère que cela vous permettra de commencer mes cours sans trop galérer.

Si vous voulez compléter votre apprentissage, et je vous conseille de le faire quitte à garder les liens et à les utiliser par après, voici quelques sites non exhaustifs qui vous permettrons d'apprendre les bases (plus ou moins par ordre de difficulté) :

http://benoit-m.developpez.com/assembleur/tutoriel | Les bases de l'asm
http://asm.developpez.com/intro/index.php | Aussi
http://www.developpez.com/ | Un site regroupant les bases de beaucoup de langages
http://www.deamoncrack.host.sk/Pages/cours-asm.htm | Si vous n'avez pas trop de temps et que vous avez juste besoin d'un rappel
http://www.chez.com/pageasm/ | Intro à la programmation win32 mais aussi DOS
http://win32assembly.online.fr/ | Le site de référence pour la programmation win32 mais en anglais
les opcodes | Très complet mais en anglais

Bon crack,

Crisanar

[ << Intro ] [ Intro à Olly >> ]

Dernière mise à jour le 04/10/2004 16:45


Cours : L'Assembleur

L'assembleur

Vu dans .... http://deamonftp.free.fr/deamoncrack/Pages/cours-asm.htm

Introduction

Ce cours vous semblera sans doute long et pénible (ça l'est également pour moi pour l'écrire ;-) ) car ce n'est pas forcément facile de comprendre tout ça, mais efforcez vous à bien vous concentrer dessus car c'est important si vous voulez devenir un bon cracker. Attention, il ne s'agit pas non plus d'apprendre tout cela par coeur. Je vous conseille de prendre des notes et de les garder à côté de vous quand vous crackez ou sinon, pour les faignants, d'imprimer ce cours disponible au format imprimable sur le Guide PDF.

Dans ce cours vous allez apprendre les bases de l'assembleur. L'assembleur c'est le langage de programmation que vous voyez quand vous désassemblez un fichier dans WinDasm. C'est un langage assez proche du langage machine.

Sommaire :

1° Le processeur et les registres
 A) Les registres généraux ou de travail
 B) Les registres d'offset ou pointeur
C) Les registres de segment
D) Le registre Flag
 2° La Pile et ses instructions
A) Fonctionnement de la pile
 B) L'instruction PUSH
C) L'instruction POP
 3° Les sauts conditionnels et le CMP
A) Les sauts conditionnels
B) L'instruction CMP
 4° Les opérations mathématiques
A) Addition et Soustraction : ADD et SUB
 B) Multiplication : MUL / IMUL
 C) Division : DIV / IDIV
 D) Autres division et multiplication : SHR et SHL
E) L'opposé d'un nombre : NEG
5° Les nombres à virgule et les nombres négatifs
A) Les nombres à virgule
 B) Les nombres négatifs
6 ° Les instructions logiques
A) ET logique : AND
B) OU logique inclusif : OR
 C) OU logique exclusif : XOR
 D) Non logique : NOT
 E) TEST
7° La mémoire et ses instructions
A) LEA
 B) MOVSx
 C) STOSx
 8° Fin du cours !

1° Le processeur et les registres

Les données dont le processeur a besoin sont stockées dans des "cases" qu'on appelle des registres. Il existe plusieurs types de registres et chacun a son utilité.

  • - registres généraux ou de travail
    Ils servent à manipuler des données, à transférer des paramètres lors de l’appel de fonction DOS et à stocker des résultats intermédiaires.

  • - registres d’offset ou pointeur
    Ils contiennent une valeur représentant un offset à combiner avec une adresse de segment

  • - registres de segment
    Ils sont utilisés pour stocker l’adresse de début d’un segment. Il peut s’agir de l’adresse du début des instructions du programme, du début des données ou du début de la pile.

  • - registre de flag
    Il contient des bits qui ont chacun un rôle indicateur.

A) Les registres généraux ou de travail

Les 4 registres généraux les plus utilisés sont : AX , BX , CX et DX.

  • AX -- accumulateur -- sert à effectuer des calculs arithmétiques ou à envoyer un paramètre à une interruption

  • BX -- registre auxiliaire de base -- sert à effectuer des calculs arithmétiques ou bien des calculs sur les adresses

  • CX -- registre auxiliaire (compteur) -- sert généralement comme compteur dans des boucles

  • DX -- registre auxiliaire de données -- sert à stocker des données destinées à des fonctions

Ceci est leur utilisation théorique, mais dans la pratique ils peuvent être utilisés à d'autres usages.

Ces registres sont de 16 bits (petit rappel : 8 bits = 1 octet et 1 bit = 0 ou 1) et chacun de ses registres est divisé en 2 parties : L (comme Low pour désigner les bits de poids faible) et H (pour High afin de désigner les bits de poids forts). Par exemple : AX (AL et AH), BX (BL et BH), CX (CL et CH), DX (DL et DH).
Pour obtenir un registre de 32 bits il faut rajouter un "E" (pour Extended, en français "étendu") devant le registre. Par exemple EAX (32 bits) pour AX (16 bits), (EBX pour BX, ECX pour CX, EDX pour DX). Il existe également des EAH ou EAL, mais la partie haute d'un registre 32 bits ne sera pas directement accessible.

Les bits correspondent en fait à leur capacité : 8 bits = nombre entre 0 et 255 (en décimal) au maximum, 16 bits = 65535 au maximum, 32 bits = 4 294 967 295 au maximum.

EAX (32 bits)
 | AX (16 bits)
 | AH (8 bits) | AL (8 bits)

B) Les registres d'offset ou pointeur

Voici les différents registres :

  • IP -- Pointeur d'instruction -- est associé au registre de segment CS (CS :IP) pour indiquer la prochaine instruction à exécuter. Ce registre ne pourra jamais être modifié directement ; il sera modifié indirectement par les instructions de saut, par les sous-programmes et par les interruptions

  • SI -- Index de source -- est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est associé au registre de segment DS.

  • DI -- Index de destination -- est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est normalement associé au registre de segment DS ; dans le cas de manipulation de chaînes de caractères, il sera associé à ES
  • SP -- Pointeur de pile -- est associé au registre de segment SS (SS :SP) pour indiquer le dernier élément de la pile.
  • BP -- Pointeur de base -- est associé au registre de segment SS (SS :BP) pour accéder aux données de la pile lors d’appels de sous-programmes (CALL).

Comme les registres généraux, ces registres peuvent être étendues à 32 bits en ajoutant le "E" : (EIP, ESI, EDI, ESP, EBP). Par contre ils ne possèdent pas de partie 8 bits (il n' y aura pas de EIH ou ESL par exemple).

Les registres SI et DI sont le plus souvent employés pour les instructions de chaîne et pour accéder à des blocs en mémoire.

Ces registres (sauf IP) peuvent apparaître comme des opérandes dans toutes les opérations arithmétiques et logiques sur 16 bits

ESI (32 bits)

SI (16 bits)

C) Les registres de segment

Le processeur utilise des adresses pour stocker ou récupérer des données. L'adresse est composée de 2 parties de 32 bits (ou 16 bits selon le mode utilisé). La première est le segment et la deuxième est l'offset. L'adresse est présenté comme cela : segment:offset (par exemple : 0A000h:00000h). Voici les différents segments :

  • CS -- Registre segment de code -- indique l’adresse du début des instructions d’un programme ou d’une sous-routine

  • DS -- Registre segment de données -- contient l’adresse du début des données de vos programmes. Si votre programme utilise plusieurs segments de données, cette valeur devra être modifié durant son exécution

  • SS -- Registre segment de pile -- pointe sur une zone appelée la pile

  • ES -- Registre segment supplémentaire (Extra segment) -- est utilisé, par défaut, par certaines instructions de copie de bloc. En dehors de ces instructions, le programmeur est libre de l’utiliser comme il l’entend

  • FS -- Registre segment supplémentaire -- Rôle semblable à ES

  • GS -- Registre segment supplémentaire -- Rôle semblable à ES

D) Le registre Flag

Ce registre est un ensemble de 16 bits. Mais cet ensemble ne signifie rien, car les bits sont modifiés un par un. Et la modification d'un seul bit peut changer le comportement du programme. Les bits de cet ensemble sont appelés "indicateurs".

Bit 1 : CF
Bit 2 : 1
Bit 3 : PF
Bit 4 : 0
Bit 5 : AF
Bit 6 : 0
Bit 7 : ZF
Bit 8 : SF
Bit 9 : TF
Bit 10 : IF
Bit 11 : DF
Bit 12 : OF
Bit 13 : IOPL
Bit 14 : NT
Bit 15 : 0
Bit 16 : RF
Bit 17 : VM

Les bits 1, 5, 12, 13, 14, 15 de ce registre Flag de 16 bits ne sont pas utilisés.

Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs.
Les instructions conditionnelles testent la valeur des indicateurs et agissent en fonction du résultat.

Voici à quoi les principaux indicateurs servent :

CF : Carry Flag - Indicateur de retenue

A la suite d'une opération, si le résultat est codé sur un nombre supérieur de bits, le bit de trop sera mis dans CF. Plusieurs instructions peuvent modifier son état : CLC pour le mettre à 0, STC à 1 et CMC inverse sa valeur.

PF : Parity Flag - Indicateur de parité

Egal à 1 si le nombre de bits d'une opérande (paramètre d'une instruction) est pair.

AF : Auxiliary carry Flag - Indicateur de retenue auxiliaire

Cet indicateur ressemble à CF.

ZF : Zero Flag - Indicateur zéro

Si le résultat d'une opération est nul (égal à 0) ZF passera à 1. Cet indicateur est souvent utilisé pour savoir si deux valeurs sont égales (en les soustrayant).

SF : Sign Flag - Indicateur de signe

SF passe à 1 quand le résultat est signé (négatif ou positif).

IF : Interruption Flag - Indicateur d'interruption

Il permet d'enlever la possibilité au processeur de contrôler les interruptions. Si IF = 1 les interruptions sont prises en compte et si IF = 0 elles sont ignorées. L'instruction STI met IF à 1 et CLI le met à 0.

DF : Direction Flag - Indicateur de direction

C'est ce Flag qui donne l'indication sur la manière de déplacer les pointeurs (références) lors des instructions de chaînes (soit positivement, soit négativement). STD le met à 1 et CLD à 0.

OF : Overflow Flag - Indicateur de dépassement

Il permet de trouver et de corriger certaines erreurs produites par des instructions mathématiques. Très utile pour éviter les plantages. Si OF=1 alors nous avons affaire à un Overflow.

Les autres indicateurs ne sont pas importants nous n'allons donc pas nous attarder dessus.

Petit exemple sur ces indicateurs avec une opération en binaire :

0 1 1 0
+ 0 1 0 1
--------
1 0 1 1
  • ZF=0 (le résultat 1011 n'est pas nul)
  • SF=1 (le signe du résultat est négatif)
  • CF=0 (il n'y a pas de retenue)

2° La Pile et ses instructions

A) Fonctionnement de la pile

La pile ("stack" en anglais) est un emplacement qui sert à stocker de petites données. Cette mémoire temporaire fonctionne comme une pile d'assiette : c'est-à-dire que la dernière assiette que vous empilez sera la première à être prise et la première assiette empilée (celle qui est tout en dessous) sera la dernière prise. J'espère avoir été clair dans cette métaphore. Pour prendre un vrai exemple, si je mets la valeur "A" dans la pile puis je mets "B" et après "C". Pour récupérer ces valeurs le processeur va d'abord prendre "C", puis "B" et ensuite "A". Là si vous ne comprenez toujours pas je ne peux plus rien pour vous !

Pour l'API GetDlgItemText, Windows a besoin de ces informations :

HWND hDlg, // (1) handle de la boîte de dialogue
int nIDDlgItem, // (2) identifiant de la boîte de dialogue
LPTSTR lpString, // (3) pointe vers le buffer pour le texte
int nMaxCount // (4) taille maximum de la chaîne

On peut donc mettre avant d'appeler cette fonction :

MOV EDI, [ESP+00000220] place l'handle de la boite de dialogue dans EDI
PUSH 00000100 push (4) taille maxi de la chaîne
PUSH 00406130 push (3) adresse du buffer pour le texte
PUSH 00000405 push (2) identificateur du contrôle
PUSH EDI push (1) hanlde de la boite de dialogue
CALL GetDlgItemText appelle la fonction

B) L'instruction PUSH

Vous avez pu voir cette instruction au dessus. Push signifie "pousser" en français. C'est avec cette instruction qu'on place une valeur sur la pile. La valeur "poussé" doit être de 16 ou 32 bits (souvent un registre) ou une valeur immédiate.

Par exemple :
PUSH AX
PUSH BX
PUSH 560
AX est placé en haut de la pile en premier mais ce sera 560 qui en ressortira en premier.

C) L'instruction POP

Mettre des données sur la pile c'est bien, mais le but c'est de les récupérer par la suite. C'est donc à ça que sert l'instruction POP : récupérer les données placées sur la pile.

Par exemple (suite à l'exemple au dessus) :
POP CX
POP BX
POP AX
Ces instructions mettront la première instruction du haut de la pile dans CX, c'est à dire 560. Et ensuite BX reviendra dans BX et AX dans AX.

La pile est très utilisée pour retrouver les valeurs intactes des registres. Car le nombre de registres est limité, donc on utilise la pile lorsque l'on n'a plus de place. Mais la pile est plus lente que les registres.

3° L es sauts conditionnels et le CMP

A) Les sauts conditionnels

Dans les différents sauts il y a les inconditionnels (qui sautent sans conditions) comme le JMP (Jump) et ceux qui sont conditionnels (qui sautent donc si la condition est vérifiée). Dans cette dernière "famille" il y a du monde. ;-) Les valeurs des indicateurs sont nécessaires pour la vérification de la condition.

Voici les principaux sauts, sachez que J = Jump if (Saut si) :

Indicateur | Valeur | Saut | Signification
CF | 1 | JB | Below - Inférieur
 |  | JBE | Below or Equal - Inférieur ou égal
 |  | JC | Carry - Retenue
 |  | JNAE | Not Above or Equal - Non supérieur ou égale
 | 0 | JA | Above - Supérieur
 |  | JAE | Above or Equal - Supérieur ou égal
 |  | JNB | Not Below - Non inférieur
 |  | JNC | Not Carry - Pas de retenue
ZF | 1 | JE | Equal - Egal
 |  | JNA | Not Above - Non supérieur
 |  | JZ | Zero - Zero (Saut en cas d'égalité)
 | 0 | JNBE | Not Below or Equal - Non supérieur ou égal
 |  | JNE | Not Equal - Non égal
 |  | JNZ | Not Zero - Non zéro (saut si différent)
PF | 1 | JP | Parity - Pair
 |  | JPE | Parity Even - Pair
 | 0 | JNP | Not Parity - Non pair
 |  | JPO | Parity Odd - Non pair
OF | 1 | JO | Overflow - Dépassement
 | 0 | JNO | Not Overflow - Pas de dépassement
SF | 1 | JS | Signed - Signé
 | 0 | JNS | Not Signed - Non signé

Et quelques autres :

JG - JNLE
Greater (arithmétiquement supérieur) - Not Less or Equal (arithmétiquement non inférieur ou égale)
ZF=0 et SF=OF

JGE - JNL
Greater or Equal (arithmétiquement supérieur ou égal) - Not Less (non inférieur arithmétiquement)
SF=OF

JL - JNGE
Less (arithmétiquement inférieur) - Not Greater or Equal (arithmétiquement non supérieur ou égal)
SF (signé)=OF

JLE - JNG
Less or Equal (arithmétiquement inférieur ou égal) - Not Greater (non supérieur arithmétiquement)
ZF=1 ou SF (signé)=OF

B) L'instruction CMP

Cette instruction permet d'effectuer des tests entre des valeurs et de modifier les flags suivant le résultat. Le fonctionnement du CMP est semblable au SUB (que l'on verra plus loin et qui sert pour la soustraction). Il s'utilise de cette façon : CMP AX, BX. Cela permet de vérifier si ces 2 valeurs sont égales car il fait AX-BX, si c'est égal à 0 alors ZF=1 sinon ZF=0.

Le plus souvent il est suivi de ces sauts et leur inverse (en rajoutant un "N" après le "J", par exemple JNE pour JE...) :

JA : plus grand que (nombres non-signés)
JB : plus petit que (nombres non-signés)
JG : plus grand que (nombres signés)
JL : plus petit que (nombres signés)
JE ou JZ : égal à (signé et non-signé)

4° L es opérations mathématiques

A) Addition et Soustraction : ADD et SUB

Ces 2 instructions nécessitent 2 opérandes : la source et la destination. Ce qui donnera : Destination = Source + Destination. Et en assembleur : ADD Destination, Source.
Exemple avec AX = 4 et BX = 6 :

ADD AX, BX

Après cette opération : AX = 4+6 = 10 et BX = 6

Le fonctionnement de SUB est identique.

Il est impossible d'ajouter ou soustraire un 16 bits avec un 8 bits par exemple. Donc ADD BX, CL sera impossible !

B) Multiplication : MUL / IMUL

L'instruction MUL est utilisé pour les nombres non signés et IMUL pour les signés (petit rappel : les nombres signés peuvent être négatifs contrairement aux non signés, par exemple en 16 bits en non signé les nombres vont de 0 à 65535 et en signé ils vont de -32768 à 32767). Contrairement aux opérations précédentes, la multiplication n'a besoin que d'une seule opérande : la source. La destination et donc le nombre qui devra être multiplié se trouvera toujours et obligatoirement dans AX. La source est un registre.

Exemple avec AX = 4 et BX = 6

MUL BX

Résultats : AX = 4*6 = 24 et BX = 6

IMUL fonctionne de la même façon, sauf qu'il faut utiliser l'instruction CWD (convert word to doubleword), qui va permettre "d'étendre" le signe de AX dans DX. Si cela n'est pas fait les résultats pourront être erronés.

C) Division : DIV / IDIV

Pareil que MUL et IMUL : DIV sert pour les nombres non signés et IDIV pour les non signés. Cette instruction divise la valeur de AX par la source.

Exemple avec AX = 18 et BX = 5 :

IDIV BX

Résultats : AX = 3 et DX = 3 (DX étant le reste)

D) Autre division et multiplication : SHR et SHL

Ces instructions permettent de diviser des registres avec SHR et les multiplier avec SHL. Ils demandent 2 opérandes : le nombre à diviser et le diviseur pour SHR et le nombre à multiplier et le multiplicateur pour SHL. Ils fonctionnent en puissance de 2. Donc si on veut diviser AX par 4 on mettra : SHR AX, 2 (car 2^2 = 4). Pour diviser BX par 16 on mettra : SHR BX,4 (car 2^4 = 16). Pour SHL c'est pareil : pour multiplier CX par 256 on mettra donc : SHL CX,8 (2^8 = 256). Pour multiplier ou diviser des nombres qui ne sont pas des puissances de 2 il faudra combiner des SHR ou SHL.
Petit rappel de mathématiques : x^0 donnera toujours 1 quelque soit x !

Exemple : on veut multiplier 5 par 384. On voit que 384 = 256 + 128, ce qui nous arrange car 256 et 128 sont des puissances de 2.

  • MOV AX,5 >> On met 5 dans AX >> AX = 5

  • MOV BX,AX >> On met AX dans BX >> BX = AX

  • SHL AX,8 >> On multiplie AX par 256 (2^8) >> AX = AX*256 = 5*256 = 1280

  • SHL BX,7 >> On multiplie BX par 128 (2^7) >> BX = BX*128 = 5*128 = 640

  • ADD AX,BX >> On ajoute BX à AX >> AX = AX + BX = 1280 + 640 = 1920

Et vous pouvez vérifier : 5*384 = 1920 ! ;-)

Mais vous me direz : "pourquoi ne pas faire simplement un MUL ?". Et bien car SHL est beaucoup plus rapide à exécuter. Mais pour des nombres qui ont une décomposition trop longue, mieux vaut bien sur utiliser un MUL.

E) L'opposé d'un nombre : NEG

L'instruction la plus simple à mon avis : NEG sert à rendre un nombre positif en négatif et inversement. Il ne demande qu'une seule opérande : la destination. Si AX = 5, alors un NEG AX mettra AX = -5. Ou si BX = -14 alors après le NEG BX, BX = 14.

5° L es nombres à virgule et les nombres négatifs

A) Les nombres à virgule

Le problème en assembleur c'est qu'on ne peut pas utiliser directement les nombres à virgule. Nous utiliserons les nombres à virgule fixe. Il s'agit simplement d'effectuer des calculs avec des valeurs assez grandes qui seront ensuite, redivisées pour retrouver un résultat dans un certain intervalle.

Par exemple nous voulons multiplier 20 par 0,25. Comme on ne peut pas mettre 0,25 dans un registre on va le mettre en nombre entier en le multipliant par 256 par exemple. 0.25*256 = 64. On le multipliera à 20 et ensuite ce résultat étant 256 fois trop grand on le redivisera par 256 :

  • MOV AX,64 >> On place 0,25*256 dans AX >> AX = 64

  • MOV BX,20 >> On place 20 dans BX >> BX = 20

  • MUL BX >> On multiplie AX par BX >> AX = AX*BX = 64*20 = 1280

  • SHR AX,8 >> On divise AX par 256 (2^8) >> AX = AX/256 = 1280/256 = 5

Le résultat de 20*0,25 est donc bien égal à 5.

Faîtes bien attention à ne pas effectuer de dépassement, surtout avec les nombres signés ou les grands nombres. En utilisant SHR ou SHL, plus l'opérande est grande et plus le résultat sera précis, car il y aura plus de décimales disponibles.

B) Les nombres négatifs

Un autre problème, c'est qu'en décimal pour un nombre négatif on place un "-" devant le nombre, mais en binaire on ne peut mettre que des 0 ou des 1. Pour pouvoir mettre un nombre en négatif on utilise la méthode du complément à 2. Voici en quoi cela consiste :

  • Convertir le nombre en binaire s'il ne l'est pas déjà

  • Inverser les bits du nombre (inverser les 0 en 1 et 1 en 0)

  • Ajouter 1 au nombre obtenu

Voici un exemple avec le nombre décimal 5 au format 8 bits (octet) :

  • On le convertit en binaire : 5(d) = 00000101(b)

  • On inverse : 00000101 --> 11111010 (ici le format, 8 bits, est important car en 4 bits par exemple cela donnera : 1010)

  • On ajoute 1 : 11111010 + 00000001 = 11111011

-5 est donc égal à 11111011 en binaire.

Pour que le nombre soit négatif il faut qu'il soit signé. En 8 bits en non signé les nombres vont de 0 à 255 et en signé ils vont de -128 à 127.

Quand le nombre est signé et en 8 bits, on peut savoir s'il est négatif en regardant le 8ème bit (en partant bien sur de la droite). S'il est égal à 1 alors le nombre est négatif. Pour les nombres à 4 bits c'est le 4ème qui compte.

Pour transformer -5 en hexadécimal, on décompose le nombre binaire en demi-octet (11111011 en 1111 et 1011). 1111b = 15d = Fh et 1011b = 11d = Ah. Donc -5d = FAh. (Petit rappel : d = décimal, b = binaire, h = hexadécimal, pour ceux qui n'auraient pas encore compris).

6° L es instructions logiques

Les opérations de ces instructions se font bit à bit. Et les opérandes et emplacements mémoires qu'elles utilisent sont en 8 ou 16 bits.

Petit rappel : pour convertir de tête un nombre binaire en décimal : chaque rang d'un nombre binaire est un multiple de 2.

2^7 | 2^6 | 2^5 | 2^4 | 2^3 | 2^2 | 2^1 | 2^0
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1

Ainsi par exemple 10110110 donnera 128+32+16+4+2 = 182 :

1 | 0 | 1 | 1 | 0 | 1 | 1 | 0
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1

A) Et logique : AND

AND (ET) effectue un ET LOGIQUE sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le ET LOGIQUE donne le résultat 1 uniquement quand les 2 opérandes sont 1, sinon il met 0.

opérande 1 | opérande 2 | AND
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

Exemple avec AX = 15 et BX = 26 :

AND AX,BX

Résultat :

0000 1111 (AX=15)
AND 0001 1010 (BX=26)
-------------
0000 1010 (AX = 10)

B) Ou logique inclusif : OR

OR effectue un OU LOGIQUE INCLUSIF sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le OU LOGIQUE INCLUSIF donne le résultat 0 uniquement quand les 2 opérandes sont 0, sinon il met 1.

opérande 1 | opérande 2 | OR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1

Exemple avec AX = 15 et BX = 26 :

OR AX,BX

Résultat :

0000 1111 (AX=15)
OR 0001 1010 (BX=26)
-------------
0001 1111 (AX = 31)

C) Ou logique exclusif : XOR

XOR effectue un OU LOGIQUE EXCLUSIF sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le OU LOGIQUE EXCLUSIF donne le résultat 1 quand les 2 opérandes sont différentes, sinon il met 0.

opérande 1 | opérande 2 | XOR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

Exemple avec AX = 15 et BX = 26 :

XOR AX,BX

Résultat :

0000 1111 (AX=15)
XOR 0001 1010 (BX=26)
-------------
0001 0101 (AX = 21)

D) Non logique : NOT

NOT effectue un NON LOGIQUE sur une opérande et le résultat se retrouvera dans l'opérande. Le NON LOGIQUE inverse la valeur de chaque bit.

opérande | NOT
0 | 1
1 | 0

Exemple avec AX = 15 :

NOT AX

Résultat :

NOT 0000 1111 (AX=15)
-------------
1111 0000 (AX = -16 en signé et 240 en non signé)

E) TEST

Cette instruction teste la valeur d’un ou plusieurs bits en effectuant un ET LOGIQUE sur les opérandes. Le résultat ne modifie pas les opérandes mais les indicateurs.

Exemple :

TEST AX,1 >> Effectue un ET LOGIQUE sur le premier bit de AX avec 1, si il est égal à 1, ZF passera à 1 et inversement (si 0 alors ZF=0).

Un TEST AX,AX (souvent utilisé) permet de voir si AX est égal à 0 ou non.

7° La mémoire et ses instructions

A) LEA

Pour placer par exemple la valeur de l'offset de MESSAGE dans SI il suffit de mettre : MOV SI,Offset-Message. Mais on ne peut pas procéder de cette façon pour les registres DS, ES, FS, GS car ceux ci n'acceptent pas les valeurs numériques, ils n'acceptent que les registres. Il faudra donc passer par : MOV AX,Seg-Message puis MOV DS,AX.

Il existe une autre instruction, LEA, qui permet de placer l'offset dans un registre mais en plus court. Par exemple : LEA SI,MESSAGE placera dans SI l'offset de Message. L'utilisation de LEA à la place de MOV rend le code plus clair et plus compact.

B) MOVSx

Pour déplacer des blocs entiers de mémoires il faut utiliser les instructions : MOVSB, MOVSW ou MOVSD selon le nombre de bits à déplacer.

  • MOVSB : (B) = Byte (Octet : 8 bits) (Attention !! En anglais "byte"= un octet et "bit"= un bit)
  • MOVSW : (W) = Word (Mot : 16 bits)
  • MOVSD : (D) = DWord (Double Mot : 32 bits)

Si on veut déplacer 1000 octets (bytes) en utilisant la commande MOVSB il faudra qu'elle se répète 1000 fois. Donc l'instruction REP est utilisée comme une boucle. Il faut placer auparavant le nombre de boucles à effectuer dans le registre de compteur (CX) :

MOV CX,1000 >> Nombre de boucles à effectuer dans le compteur CX
REP MOVSB >> Déplace un octet

Pour aller plus vite on peut les déplacer par Mot (Word) :

MOV CX,500 >> 1000 bytes = 500 words
REP MOVSW >> Déplace un mot

Ou alors en Double Mot (DWord) :

MOV CX,250 >> 1000 bytes = 500 words = 250 dwords
REP MOVSD >> Déplace un Double Mot

A chaque MOVSx, DI augmente de 1,2 ou 4 bytes

C) STOSx

Pour garder des données ou les placer dans un emplacement de la mémoire on utilise les instructions STOSB, STOSW ou STOSD.

Pareil que les MOVSx, ces instructions servent respectivement à stocker un byte, un word et un dword. Par contre ils utilisent AL, AX (EAX 32bits) comme valeur à stocker. La valeur de AL,AX,EAX sera stockée dans ES:DI. A chaque STOSx, DI est augmenté de 1,2 ou 4 bytes.

Exemple :

MOV CX,10 >> nombre de répétitions placées dans CX, ici 10
MOV AX,0 >> valeur à déplacer, ici 0
REP STOSB >> stocker AX dans ES:DI

Nous aurons donc 10 bytes de 0 dans ES:DI.

8° Fin du cours !

Voilà c'est la fin du cours ! "Ouf" diront certains, "Oh quel dommage" diront les hypocrites ! ;-) Je félicite ceux qui ont réussi à lire tout ça ! ;-) J'espère que cela vous servira à mieux comprendre en crackant. Avec ça vous devriez vous sentir plus à l'aise (enfin si vous avez compris mon cours).

J'espère avoir été le plus compréhensible possible. Mais si vous ne comprenez pas quelque chose à propos de ce cours demandez sur le forum plutôt que de m'envoyer un mail sur deamon.crack (at) netcourrier.com car j'essaye de toute façon de lire tous les posts du forum et d'autres personnes que moi pourront vous aider.

Je voulais féliciter le très bon cours de Falcon sur l'assembleur qui m'a permis de découvrir l'assembleur. D'ailleurs j'ai repris un peu son plan, car je n'ai pas réussi à en faire un bien, et j'ai dû copier seulement 2-3 phrases. Mais c'est bien moi qui ai écrit tout ça, en 6-8 heures (et oui c'est très long à écrire tout ça) !

Pendant que je suis là ;-) , je remercie également tout ceux qui viennent sur mon site, ceux qui m'envoient des mails de sympathie et ceux qui participent plus ou moins activement sur le forum.

Deamon, le 15-16 juillet 2003


Apprendre l

Apprendre l'assembleur

L'auteur

Benoît-M
http://www.developpez.net/forums/u1100/benoit-m/ http://www.developpez.net/forums/u1100/benoit-m/

L'article
Publié le 3 janvier 2003 - Mis à jour le 15 février 2013 http://benoit-m.developpez.com/assembleur/tutoriel

  • I. Introduction
  • II. PREMIÈRE PARTIE
    • II-A. L'ARITHMÉTIQUE SIGNÉE
    • II-B. LA MÉMOIRE VIVE
      • II-B-1. La segmentation de la mémoire
      • II-B-2. Structure d'un programme en mémoire
      • II-B-3. Les fichiers COM
      • II-B-4. Les fichiers EXE
      • II-B-5. Les cycles de lecture-écriture
    • II-C. LE MICROPROCESSEUR - LES REGISTRES
      • II-C-1. Généralités sur les registres
      • II-C-2. Les registres généraux
      • II-C-3. Les registres de segment
      • II-C-4. Les registres d'offset
      • II-C-5. Le registre des indicateurs
    • II-D. LES INTERRUPTIONS
      • II-D-1. Introduction
      • II-D-2. La table des vecteurs d'interruptions
      • II-D-3. Sauvegarde de l'état des registres lors de l'appel
  • III. DEUXIÈME PARTIE
    • III-A. PREMIER EXEMPLE : LES FICHIERS COM
    • III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE
  • IV. TROISIÈME PARTIE
    • IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE
      • IV-A-1. Les définitions de données
      • IV-A-2. L'adressage
        • IV-A-2-a. L'adressage immédiat
        • IV-A-2-b. L'adressage indexé et/ou basé
    • IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS
      • IV-B-1. Les sauts inconditionnels
        • IV-B-1-a. Les procédures
          • IV-B-1-a-i. appels de procédures
          • IV-B-1-a-ii. le passage des paramètres
        • IV-B-1-b. Les macros
        • IV-B-1-c. La directive EQU
        • IV-B-1-d. L'inclusion de fichiers
    • IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE
      • IV-C-1. L'instruction NOP (« No Operation »)
      • IV-C-2. L'instruction MOV (« Move »)
      • IV-C-3. L'instruction XCHG (« Exchange »)
      • IV-C-4. L'instruction JMP (« Jump »)
      • IV-C-5. L'opérateur CMP (« Compare »)
      • IV-C-6. Les instructions de saut conditionnel
        • IV-C-6-a. les sauts de comparaison
          • IV-C-6-a-i. les sauts de test sur les flags
          • IV-C-6-a-ii. le saut de test sur le registre CX
        • IV-C-6-b. Les instructions arithmétiques
          • IV-C-6-b-i. L'instruction INC (« Increment »)
          • IV-C-6-b-ii. L'instruction ADD (« Addition »)
          • IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)
          • IV-C-6-b-iv. L'instruction DEC (« Decrement »)
          • IV-C-6-b-v. L'instruction SUB (« Substract »)
          • IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)
          • IV-C-6-b-vii. L'instruction MUL (« Multiply »)
          • IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)
          • IV-C-6-b-ix. L'instruction DIV (« Divide »)
          • IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)
          • IV-C-6-b-xi. L'instruction NEG (« Negation »)
        • IV-C-6-c. Les instructions logiques
          • IV-C-6-c-i. L'instruction NOT (« Logical NOT »)
          • IV-C-6-c-ii. L'instruction OR (« Logical OR »)
          • IV-C-6-c-iii. L'instruction AND (« Logical AND »)
          • IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)
          • IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)
          • IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)
          • IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)
          • IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)
          • IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)
          • IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)
          • IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)
        • IV-C-6-d. Les instructions de manipulation des flags
          • IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)
          • IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)
          • IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)
          • IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)
          • IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)
          • IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)
          • IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)
          • IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)
          • IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)
        • IV-C-6-e. Les instructions de gestion de la pile
          • IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)
          • IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)
          • IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)
          • IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)
          • IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)
          • IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)
        • IV-C-6-f. Les instructions de gestion des chaînes d'octets
          • IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)
          • IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)
          • IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)
        • IV-C-6-g. Les instructions de gestion des chaînes de mots
          • IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)
          • IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)
          • IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)
          • IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)
          • IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)
        • IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots
          • IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)
          • IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)
          • IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)
          • IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)
          • IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)
        • IV-C-6-i. Les instructions d'appel
          • IV-C-6-i-i. L'instruction CALL (« Procedure Call »)
          • IV-C-6-i-ii. L'instruction RET (ou RETN)
          • IV-C-6-i-iii. L'instruction RETF
        • IV-C-6-j. Les instructions de boucle
          • IV-C-6-j-i. L'instruction LOOP
          • IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)
          • IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)
        • IV-C-6-k. Les instructions d'adressage
          • IV-C-6-k-i. L'instruction LEA (« Load effective address »)
          • IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)
          • IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)
        • IV-C-6-l. Les instructions de conversion arithmétique
          • IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
          • IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
          • IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
          • IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
          • IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)
          • IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)
          • IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
          • IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
          • IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)
          • IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)
          • IV-C-6-l-xi. L'instruction XLAT (« Translate »)
        • IV-C-6-m. Les instructions d'entrée-sortie
          • IV-C-6-m-i. L'instruction IN (« Input from port »)
          • IV-C-6-m-ii. L'instruction OUT (« Output to port »)
  • V. QUATRIÈME PARTIE
    • V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES
      • V-A-1. Ouverture d'un fichier
      • V-A-2. Lecture dans un fichier
      • V-A-3. Écriture dans un fichier
      • V-A-4. Existence d'un pointeur de fichier
      • V-A-5. Fermeture d'un fichier
      • V-A-6. Conclusion
    • V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS
      • V-B-1. La fonction 4eh
      • V-B-2. La fonction 4fh
      • V-B-3. Conclusion
      • V-B-4. EXEMPLE DE PROGRAMME
      • V-B-5. Entrée bufferisée au clavier
      • V-B-6. Le programme
  • VI. CONCLUSION

L'auteur

Benoît-M

L'article

Publié le 3 janvier 2003 - Mis à jour le 15 février 2013


I

I. Introduction

« Désormais, pour apprendre le français, il faudra SAVOIR le français ! » disait Coluche. De même, ce tutoriel s'adresse à tous ceux qui connaissent déjà les rudiments d'un langage évolué tel que le BASIC, le FORTRAN, le C ou le PASCAL et qui souhaitent apprendre l'assembleur. Une connaissance même sommaire d'un de ces langages suffit ! Aucune connaissance en programmation système n'est requise : le but de ce cours est justement d'en introduire les fondements.

Contrairement aux langages évolués, l'assembleur, ou « langage d'assemblage » est constitué d'instructions directement compréhensibles par le microprocesseur : c'est ce qu'on appelle un langage de bas niveau. Il est donc intimement lié au fonctionnement de la machine. C'est pourquoi il est relativement difficile à assimiler, en tout cas beaucoup plus que les langages de haut niveau.

Cela explique également pourquoi il existe au moins autant de langages d'assemblage que de modèles de microprocesseurs.

Avant d'apprendre l'assembleur INTEL 80x86, il est donc primordial de s'intéresser à quelques notions de base concernant par exemple la mémoire ou le microprocesseur. C'est là en effet que se trouvent les principales difficultés pour le débutant. Ne soyez pas rebuté par l'abstraction des concepts présentés dans les premiers paragraphes : il est normal que durant la lecture, beaucoup de choses ne soient pas claires dans votre esprit. Tout vous semblera beaucoup plus simple quand nous passerons à la pratique dans le langage assembleur.

Le microprocesseur peut fonctionner sous deux modes : le mode réel et le mode protégé. Le mode protégé permet d'accéder à 232 octets de mémoire vive, alors que le mode réel ne peut en adresser que 220 = 1 Mo. Nous ne traiterons dans ce cours que le mode réel. C'est celui qu'utilisent la plupart des programmes DOS.

Convention : toutes les adresses sont écrites en notation hexadécimale. Les autres nombres seront la plupart du temps représentés en base décimale. Dans le cas contraire, nous ajouterons la lettre ‘h' après les chiffres.


II

II. PREMIÈRE PARTIE

II-A. L'ARITHMÉTIQUE SIGNÉE

On appelle arithmétique non signée l'arithmétique dans laquelle tous les entiers sont positifs. En arithmétique signée au contraire, les nombres peuvent être soit positifs, soit négatifs. Un nombre signé n'est donc pas forcément négatif.

Les données informatiques se présentent sous la forme d'une succession de chiffres binaires, les bits. Nous supposerons que les systèmes de numération binaire et hexadécimal vous sont déjà familiers.

Il est en revanche fort possible que vous ne connaissiez pas la façon dont un ordinateur représente les nombres négatifs. Il existe deux conventions : la notation en signe et valeur absolue et la notation en complément à 2.

La première est extrêmement simple : le premier bit représente le signe, et les autres bits la valeur absolue du nombre. Le bit de signe vaut 1 si le nombre est strictement négatif, 0 sinon.

Par exemple, le nombre (-14) codé sur 8 bits s'écrit ainsi :

10001110

Cette convention n'est quasiment jamais utilisée en informatique. On lui préfère la représentation en complément à 2 dont le principe est le suivant :

  • les nombres positifs sont codés de la même façon qu'en convention « signe et valeur absolue » ;
  • les nombres négatifs sont obtenus en inversant tous les bits, puis en ajoutant 1.

Exemple : le nombre 14 codé sur 8 bits est représenté ainsi :

00001110

et (-14) ainsi :

  • inversion des bits : 11110001 ;
  • ajout d'une unité : 11110010 ;
  • résultat : 11110010.

Remarque : le résultat intermédiaire, 11110001, est appelé « complément à 1 ».

Vous allez immédiatement comprendre l'avantage de cette représentation. Faisons la somme de 14 et de (-14), de la même façon que s'il s'agissait d'entiers positifs :

00001110 + 11110010 = 100000000

Le résultat étant codé sur 8 bits, le 1 situé à gauche n'est pas pris en compte. On obtient donc 14 + (-14) = 0.

L'intérêt évident est que la différence de deux nombres peut se calculer avec le même algorithme que leur somme. Il suffit de transformer au préalable le nombre retranché en son opposé. Cette conversion est très simple et très rapide.

Au contraire, en représentation « signe et valeur absolue », on aurait eu besoin de nombreux algorithmes, car plusieurs cas se présentent.

Remarque : la représentation en complément à 2 revient en fait à écrire (-1) comme ceci :

11111111

(-2) comme cela :

11111110

(-3) comme cela :

11111101

etc.

Il y a ainsi une symétrie entre les nombres positifs et les nombres négatifs. Il en résulte que le bit le plus à gauche représente le signe, de la même façon qu'en notation « signe et valeur absolue ». Avant tout calcul, nous pouvons donc affirmer que le nombre 10010101 est négatif.

II-B. LA MÉMOIRE VIVE

II-B-1. La segmentation de la mémoire

Votre PC est conçu pour gérer 1 Mo (soit 220 octets) de mémoire vive en mode réel. Il faut donc 20 bits au minimum pour adresser toute la mémoire. Or en mode réel les bus d'adresses n'ont que 16 bits. Ils permettent donc d'adresser 216 = 65536 octets = 64 Ko, ce qui est insuffisant !

Afin de pallier ce manque, on utilise deux nombres pour adresser un octet quelconque de la RAM. Le premier est appelé adresse de segment, le second adresse d'offset. Ils seront stockés séparément.

La mémoire est découpée en segments de 64 Ko chacun. Un segment est donc en quelque sorte un gros bloc de mémoire auquel on peut accéder grâce à une adresse de segment qui désigne son numéro. Par exemple, le premier segment est le segment 0000 (en hexa), le deuxième est le 0001, le quarante-deuxième est le 0029, etc. Chaque numéro est codé sur 16 bits, c'est-à-dire 4 chiffres hexa.

Pour accéder à un octet particulier dans un segment, il suffit de compter le décalage de cet octet par rapport au début du segment. Ce décalage est obligatoirement inférieur ou égal à 65535 : il tient bien sur 16 bits lui aussi. On appelle ce décalage « offset ».

L'adresse d'un octet se note XXXX:YYYY où XXXX est l'adresse de segment et YYYY est l'offset (tous deux en notation hexadécimale, bien sûr).

Par exemple, le dix-septième octet de la RAM (le numéro 16) est situé à l'adresse 0000:0010. De même, l'octet 0000:0100 est l'octet numéro 256. Nous en arrivons à la petite subtilité qu'il convient de bien saisir, sous peine de ne rien comprendre à certains programmes en assembleur.

On pourrait penser que l'octet qui se trouve à l'adresse 0001:0000 est le numéro 65536. Il n'en est rien. C'est l'octet numéro 16.

Eh oui ! Les segments ne sont pas situés gentiment les uns à la suite des autres. Ils sont fort mal élevés et n'attendent pas que les segments qui les précèdent soient terminés avant de commencer ! Ils se marchent donc sur les pieds.

Autrement dit, le deuxième segment ne démarre pas à l'octet 65536 comme il devrait le faire s'il était bien sage, mais à l'octet 16 ! Le troisième démarre à l'octet 32 et ainsi de suite…

La notion de segment n'est pas tant physique que mathématique : elle sert à se repérer dans la RAM.

La conséquence immédiate de tout cela est qu'un octet n'a pas une adresse unique. Par exemple, l'octet numéro 66 peut être adressé par 0000:0042, mais aussi par 0001:0032, par 0002:0022, par 0003:0012 ou encore par 0004:0002. Toutes ces adresses sont équivalentes.

Voilà pour la subtilité. Si vous avez compris, vous devriez être capable de trouver facilement comment on calcule l'adresse effective d'un octet, c'est-à-dire sa position absolue dans la RAM.

Allez, un petit effort !

Voici la solution : si l'adresse de l'octet est A17C:022E, alors son adresse effective est A17C x 16 + 022E, soit A17C0 + 022E = A19EE. On a multiplié par 16, car le segment A17C débute à l'octet A17C x 16, puis on a simplement ajouté le décalage.

Au final, on a bien une adresse sur 20 bits puisqu'on obtient 5 chiffres hexa. Chaque petit bloc de 16 octets s'appelle un paragraphe.

Image non disponible

Il est souvent plus simple de considérer qu'un segment est un bloc de taille quelconque qui débute à une adresse effective multiple de 16 et qui permet, à l'aide de son adresse de segment et d'un offset, d'adresser le bloc entier (64 Ko au maximum). Cette définition est à notre avis celle qui a le plus de sens, et nous l'utiliserons tout au long de ce cours.

II-B-2. Structure d'un programme en mémoire

Lorsque l'utilisateur exécute un programme, celui-ci est d'abord chargé en mémoire par le système. Le DOS distingue deux modèles de programmes exécutables : les fichiers COM et les fichiers EXE.

La différence fondamentale est que les programmes COM ne peuvent pas utiliser plus d'un segment dans la mémoire. Leur taille est ainsi limitée à 64 Ko. Les programmes EXE ne sont quant à eux limités que par la mémoire disponible dans l'ordinateur.

II-B-3. Les fichiers COM

Lorsqu'il charge un fichier COM, le DOS lui alloue toute la mémoire disponible. Si celle-ci est insuffisante, il le signale à l'utilisateur par un message et annule toute la procédure d'exécution. Dans le cas contraire, il crée le PSP du programme au début du bloc de mémoire réservé, et copie le programme à charger à la suite.

Mais qu'est-ce donc qu'un PSP ?

Pour simplifier, le PSP (« Program Segment Prefix ») est une zone de 256 (= 100h) octets qui contient des informations diverses au sujet du programme. C'est dans le PSP que se trouve la ligne de commande tapée par l'utilisateur. Par exemple, le PSP d'un programme appelé MONPROG, exécuté avec la commande « MONPROG monfic.txt /S /H », contiendra la chaîne de caractères suivante : « monfic.txt /S /H ». Le programmeur a ainsi la possibilité d'accéder aux paramètres.

Voici à titre indicatif la structure simplifiée du PSP (ne vous souciez pas de ce que vous ne comprenez pas : pour l'instant, seules les deux dernières lignes nous intéressent vraiment) :

Offset | Description | Taille (octets)
00 h | Appel de l'int 20h | 2
02 h | Adresse du 1er segment qui se trouve au-delà du prog. | 2
04 h | Réservé | 1
05 h | Far call de l'int 21h (inutilisé) | 5
0A h | Vecteur de l'int 22h | 4
0E h | Vecteur de l'int 23h | 4
12 h | Vecteur de l'int 24h | 4
16 h | Réservé | 22
2C h | Segment du bloc d'environnement | 2
2E h | Réservé | 82
80 h | Nombre de caractères dans la ligne de commande sans compter le code ASCII 13 (retour chariot) | 1
81 h | Ligne de commande (à partir du caractère espace qui suit le nom du programme) + code ASCII 13 | 127

À présent que nous connaissons l'existence du PSP, il nous faut revenir sur un point important. Comme nous l'avons dit, un programme COM ne peut comporter qu'un seul segment, bien que le DOS lui réserve la totalité de la mémoire disponible. Ceci a deux conséquences. La première est que les adresses de segment sont inutiles dans le programme : les offsets seuls permettent d'adresser n'importe quel octet du segment. La seconde est que le PSP fait partie de ce segment, ce qui limite à 64 Ko - 256 octets la taille maximale d'un fichier COM. Cela implique également que le programme lui-même débute à l'offset 100h et non à l'offset 0h.

II-B-4. Les fichiers EXE

« Pour un fichier EXE, tout est toujours un peu plus compliqué. »
(Devise du programmeur débutant en assembleur)

Bien qu'il soit possible de n'utiliser qu'un seul segment à tout faire, la plupart des programmes EXE ont un segment réservé au code (c'est ainsi qu'on appelle les instructions du langage machine), un ou deux autres aux données, et un dernier à la pile.

La pile est une mémoire très spéciale qui sert comme son nom l'indique à empiler des données de 16 bits de façon temporaire. On peut ensuite retrouver ces données en les dépilant. Le « dépilage » se fait toujours dans l'ordre inverse de l'empilage.

Le PSP a lui aussi son propre segment. Le programme commence donc à l'offset 0h du segment de code et non à l'offset 100h.

Afin que le programme puisse être chargé et exécuté correctement, il faut que le système sache où commence et où s'arrête chacun de ces segments. À cet effet, les compilateurs créent un en-tête (ou « header ») au début de chaque fichier EXE. Ce header ne sera pas copié en mémoire. Son rôle est simplement d'indiquer au DOS (lors du chargement) la position relative de chaque segment dans le fichier.

II-B-5. Les cycles de lecture-écriture

Le code et les variables d'un programme se trouvent dans la mémoire vive. Pour accéder à une donnée en mémoire, le processeur place son adresse dans un bus d'adresse. Un cycle de lecture se met alors en place. Il consiste à retourner la donnée lue au processeur via le bus de données.

Pour l'écriture, l'adresse de la destination est transmise dans le bus d'adresse et la donnée à écrire est placée dans le bus de données.

Ouf !

Nous aurons l'occasion de revenir sur la mémoire vive dans les autres chapitres pour apporter quelques précisions. Vous devriez mieux comprendre toutes ces notions en lisant la suite, car jusqu'ici nous n'avons pas encore parlé de la façon dont l'ordinateur se sert des adresses pour accéder à des données ou pour exécuter du code. C'est là qu'intervient le microprocesseur…

II-C. LE MICROPROCESSEUR - LES REGISTRES

II-C-1. Généralités sur les registres

Le microprocesseur est le cœur de l'ordinateur. C'est lui qui est chargé de reconnaître les instructions et de les exécuter (ou de les faire exécuter). Chaque instruction se présente sous la forme d'une suite de bits qu'on représente en notation hexadécimale (exemple : B44C, ou 1011010001001100 en binaire). Une instruction se compose d'un code opérateur (le plus souvent appelé « opcode ») qui désigne l'action à effectuer (B4 dans notre exemple) et d'un « champ d'opérandes » sur lesquels porte l'opération (ici l'opérande est 4C).

Pour travailler, le microprocesseur utilise de petites zones où il peut stocker des données. Ces zones portent le nom de registres et sont très rapides d'accès puisqu'elles sont implantées dans le microprocesseur lui-même et non dans la mémoire vive.

Les registres des processeurs INTEL se classent en quatre catégories :

  • les registres généraux (16 bits) ;
  • les registres de segment (16 bits) ;
  • les registres d'offset (16 bits) ;
  • le registre des indicateurs (16 bits).

II-C-2. Les registres généraux

Ils ne sont pas réservés à un usage très précis, aussi les utilise-t-on pour manipuler des données diverses. Ce sont en quelque sorte des registres à tout faire. Chacun de ces quatre registres peut servir pour la plupart des opérations, mais ils ont tous une fonction principale qui les caractérise.
Nom | Fonction privilégiée
AX | Accumulateur
BX | Base
CX | Compteur
DX | Données

Le registre AX sert souvent de registre d'entrée-sortie : on lui donne des paramètres avant d'appeler une fonction ou une procédure. Il est également utilisé pour de nombreuses opérations arithmétiques, telles que la multiplication ou la division de nombres entiers. Il est appelé « accumulateur ».

Exemples d'utilisation :

  • l'instruction “MOV AX, 1982” place l'entier 1982 dans AX ;
  • “ADD AX, 1983” ajoute à AX le nombre 1983 et place le résultat dans AX.

Le registre BX peut servir de base. Nous verrons plus tard ce que ce terme signifie.

Le registre CX est utilisé comme compteur dans les boucles. Par exemple, pour répéter 15 fois une instruction en assembleur, on peut mettre la valeur 15 dans CX, écrire l'instruction précédée d'une étiquette qui représente son adresse en mémoire, puis faire un LOOP à cette adresse. Lorsqu'il reconnaît l'instruction LOOP, le processeur « sait » que le nombre d'itérations à exécuter se trouve dans CX. Il se contente alors de décrémenter CX, de vérifier que CX est différent de 0 puis de faire un saut (« jump ») à l'étiquette mentionnée. Si CX vaut 0, le processeur ne fait pas de saut et passe à l'instruction suivante.

Exemple :

Image non disponible

Le registre DX contient souvent l'adresse d'un tampon de données lorsqu'on appelle une fonction du DOS. Par exemple, pour écrire une chaîne de caractères à l'écran, il faut placer l'offset de cette chaîne dans DX avant d'appeler la fonction appropriée.

Chacun de ces quatre registres comporte 16 bits. On peut donc y stocker des nombres allant de 0 à 65535 en arithmétique non signée, et des nombres allant de -32768 à 32767 en arithmétique signée. Les 8 bits de poids fort d'un registre sont appelés « partie haute » et les 8 autres « partie basse ». Chaque registre est en fait constitué de deux « sous-registres » de 8 bits. La partie haute de AX s'appelle AH, sa partie basse AL. Il en va de même pour les trois autres.

En arithmétique non signée, il en découle la formule suivante :

AX = AH x 256 + AL

ainsi que leurs homologues :

BX = BH x 256 + BL

CX = CH x 256 + CL

DX = DH x 256 + DL

Il va de soi qu'en modifiant AL ou AH, on modifie également AX.

Pour stocker un nombre de 32 bits, on peut utiliser des paires de registres. Par exemple, DX:AX signifie DX x 65535 + AX en arithmétique non signée.

Cette notation (DX:AX) n'est pas reconnue par l'assembleur (ni par la machine). Ne confondez pas cela avec les adresses de segment et d'offset.

Sur un PC relativement récent, AX, BX, CX, DX ne sont en fait que les parties basses de registres de 32 bits nommés EAX, EBX, ECX, EDX (« E » pour « Extended »). On a donc un moyen plus pratique de stocker les grands nombres. En fait, chaque registre (pas seulement les registres généraux) peut contenir 32 bits.

II-C-3. Les registres de segment

Ils sont au nombre de quatre :
Nom | Nom complet | Traduction
CS | Code segment | Segment de code
DS | Data segment | Segment de données
ES | Extra segment | Extra segment
SS | Stack segment | Segment de pile

Contrairement aux registres généraux, ces registres ne peuvent servir pour les opérations courantes : ils ont un rôle très précis. On ne peut d'ailleurs pas les utiliser aussi facilement que AX ou BX, et une petite modification de l'un d'eux peut suffire à « planter » le système. Eh oui ! L'assembleur, ce n'est pas Turbo Pascal ! Il n'y a aucune barrière de protection, si bien qu'une petite erreur peut planter le DOS. Mais rassurez-vous : tout se répare très bien en redémarrant l'ordinateur…

Dans le registre CS est stockée l'adresse de segment de la prochaine instruction à exécuter. La raison pour laquelle il ne faut surtout pas changer sa valeur directement est évidente. De toute façon, vous ne le pouvez pas. Le seul moyen viable de le faire est d'utiliser des instructions telles que des sauts (JMP) ou des appels (CALL) vers un autre segment. CS sera alors automatiquement actualisé par le processeur en fonction de l'adresse d'arrivée.

Le registre DS est quant à lui destiné à contenir l'adresse du segment des données du programme en cours. On peut le faire varier à condition de savoir exactement pourquoi on le fait. Par exemple, on peut avoir deux segments de données dans son programme et vouloir accéder au deuxième. Il faudra alors faire pointer DS vers ce segment.

ES est un registre qui sert à adresser le segment de son choix. On peut le changer aux mêmes conditions que DS. Par exemple, si on veut copier des données d'un segment vers un autre, on pourra faire pointer DS vers le premier et ES vers le second.

Le registre SS adresse le segment de pile. Il est rare qu'on doive y toucher car le programme n'a qu'une seule pile.

Intéressons-nous à présent aux valeurs que le DOS donne à ces registres lors du chargement en mémoire d'un fichier exécutable ! Elles diffèrent selon que le fichier est un programme COM ou EXE. Pour écrire un programme en assembleur, il est nécessaire de connaître ce tableau par cœur :

Registre | Valeur avant l'exécution
 | Fichier COM | Fichier EXE
CS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de code
DS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
ES | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
SS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de pile

Le schéma suivant montre mieux la situation :

Image non disponible

Dans un fichier EXE, le header indique au DOS les adresses initiales de chaque segment par rapport au début du programme (puisque le compilateur n'a aucun moyen de connaître l'adresse à laquelle le programme sera chargé). Lors du chargement, le DOS ajoutera à ces valeurs l'adresse d'implantation pour obtenir ainsi les véritables adresses de segment.
Dans le cas d'un fichier COM, tout est plus simple. Le programme ne comporte qu'un seul segment, donc il suffit tout bêtement au DOS de charger CS, DS, ES et SS avec l'adresse d'implantation.

Remarque : Pourquoi DS et ES pointent-ils vers le PSP dans le cas d'un fichier EXE ? Première raison : pour que le programmeur puisse accéder au PSP ! Deuxième raison : parce qu'un programme EXE peut comporter un nombre quelconque de segments de données. C'est donc au programmeur d'initialiser ces registres, s'il veut accéder à ses données.

II-C-4. Les registres d'offset

Les voici :
Nom | Nom complet | Traduction
IP | Instruction pointer | Pointeur d'instruction
SP | Stack pointer | Pointeur de pile
SI | Source index | Index de source
DI | Destination index | Index de destination
BP | Base pointer | Pointeur de base

Le registre IP désigne l'offset de la prochaine instruction à exécuter, par rapport au segment adressé par CS. La combinaison de ces deux registres (i.e. CS:IP) suffit donc à connaître l'adresse absolue de cette instruction. Le processeur peut alors aller la chercher en mémoire et l'exécuter. De plus, il actualise IP en l'incrémentant de la taille de l'instruction en octets. Tout comme CS, il est impossible de modifier IP directement.

Le registre SP désigne le sommet de la pile. Il faut bien comprendre le fonctionnement de la pile, aussi allons-nous insister sur ce point. La pile ne peut stocker que des mots. On appelle un mot (« word » en anglais) un nombre codé sur deux octets (soit 16 bits). Prenons un exemple simple : un programme COM. Le segment de pile (adressé par SS) et le segment de code ne font qu'un. Avant l'exécution, SP vaut FFFE. C'est l'offset de la dernière donnée de 16 bits empilée, par rapport à SS bien sûr. Pourquoi FFFE ? Tout simplement parce que la pile se remplit à l'envers, c'est-à-dire en partant de la fin du segment et en remontant vers le début. Le premier mot empilé se trouve à l'offset FFFE. Il tient sur deux octets : l'octet FFFE et l'octet FFFF. Mais comment se fait-il qu'un mot soit déjà empilé avant le début du programme ? Ce mot est un zéro que le DOS place sur la pile avant l'exécution de tout programme COM. Nous en verrons la raison plus tard.

À présent, que se passe-t-il si à un instant quelconque une instruction ordonne au processeur d'empiler un mot ? Eh bien le stack pointer sera décrémenté de 2 et le mot sera copié à l'endroit pointé par SP. Rappelez-vous que la pile se remplit à l'envers ! C'est pour cette raison que SP est décrémenté à chaque empilage et non pas incrémenté.

Un petit exemple pour rendre les choses plus concrètes :

PUSH AX

L'effet de cette instruction est d'empiler le mot contenu dans le registre AX. Autrement dit, SP est automatiquement décrémenté de 2, puis AX est copié à l'adresse SS:SP.

Lors du dépilage, le mot situé au sommet de la pile, c'est-à-dire le mot adressé par SS:SP, est transféré dans un registre quelconque choisi par le programmeur, après quoi le stack pointer est incrémenté de 2.

Exemple :

POP BX

Cette fois, on retire le dernier mot empilé pour le placer dans le registre BX. Évidemment, SP sera incrémenté de 2 aussitôt après.

La pile est extrêmement utile lorsqu'il s'agit de stocker provisoirement le contenu d'un registre qui doit être modifié.

Exemple :

Image non disponible

Il est important de comprendre qu'on ne peut dépiler que le mot qui se trouve au sommet de la pile. Le premier mot empilé est le dernier qui sera dépilé. La pile doit être manipulée avec une extrême précaution. Un dépilage injustifié fait planter la machine presque systématiquement.

Les trois derniers registres sont beaucoup moins liés au fonctionnement interne du processeur. Ils sont mis à la disposition du programmeur qui peut les modifier à sa guise et les utiliser comme des registres généraux. Comme ces derniers cependant, ils ont une fonction qui leur est propre : servir d'index (SI et DI) ou de base (BP). Nous allons expliciter ces deux termes.

Dans la mémoire, les octets se suivent et forment parfois des chaînes de caractères. Pour utiliser une chaîne, le programmeur doit pouvoir accéder facilement à tous ses octets, l'un après l'autre. Or pour effectuer une opération quelconque sur un octet, il faut connaître son adresse. Cette adresse doit en général être une constante évaluable par le compilateur. Pourquoi une constante ? Parce que l'adresse est un opérande comme les autres, elle se trouve immédiatement après l'opcode et doit donc avoir une valeur numérique fixe !

Prenons un exemple :

MOV AH, [MonOctet]

Pas de panique ! Cette instruction en assembleur signifie « Mettre dans AH la valeur de l'octet adressé par le label MonOctet ! ». À la compilation, « MonOctet » sera remplacé par la valeur numérique qu'il représente et on obtiendra alors une instruction en langage machine telle que :

8A260601

8A26 est l'opcode (hexa) de l'instruction “MOV AH, [constante quelconque]”, et 0601 est l'offset de « MonOctet ».

Il serait pourtant fastidieux, dans le cas d'une chaîne de 112 caractères, de traiter les octets avec 112 instructions dans lesquelles seule l'adresse changerait. Il faudrait pouvoir faire une boucle sur l'adresse, mais alors celle-ci ne serait plus une constante, d'où le problème.

Pour les constructeurs du microprocesseur, la seule solution était de créer de nouveaux opcodes pour chaque opération portant sur un octet en mémoire. Ces opcodes spéciaux feraient la même action que ceux dont ils seraient dérivés, mais l'adresse passée en paramètre serait alors considérée comme un décalage par rapport à un registre spécial. Il suffirait donc de faire varier ce registre, et le processeur y ajouterait automatiquement la valeur de l'opérande pour obtenir l'adresse réelle ! C'est à cela que servent SI, DI et BP.

Par exemple :

MOV AH, [MonOctet + DI]

sera codé :

8AA50601

8AA5 est l'opcode pour l'instruction “MOV AH, [constante quelconque + DI]”.

Remarque : les registres SI et BP auraient tout aussi bien pu être employés, mais pas les registres généraux, SAUF BX. En effet, BX peut jouer exactement le même rôle que BP. N'oubliez pas que BX est appelé registre de « base », et que BP signifie « Base Pointer. » Nous verrons la différence entre une base et un index lorsque nous commencerons l'assembleur.

II-C-5. Le registre des indicateurs

Un programme doit pouvoir faire des choix en fonction des données dont il dispose. Pour cela, il lui faut par exemple comparer des nombres, examiner leur signe, découvrir si une erreur a été constatée, etc.

Il existe à cet effet de petits indicateurs, les flags qui sont des bits spéciaux ayant une signification très précise. De manière générale, les flags fournissent des informations sur les résultats des opérations précédentes.

Ils sont tous regroupés dans un registre : le registre des indicateurs. Comprenez bien que chaque bit a un rôle qui lui est propre et que la valeur globale du registre ne signifie rien. Le programmeur peut lire chacun de ces flags et parfois modifier leur valeur directement.

En mode réel, certains flags ne sont pas accessibles. Nous n'en parlerons pas. Nous ne commenterons que les flags couramment utilisés.

Nous verrons quelle utilisation on peut faire de ces indicateurs dans la troisième partie de ce tutoriel.

  • CF (« Carry Flag ») est l'indicateur de retenue. Il est positionné à 1 si et seulement si l'opération précédemment effectuée a produit une retenue. De nombreuses fonctions du DOS l'utilisent comme indicateur d'erreur : CF passe alors à 1 en cas de problème.
  • PF (« Parity Flag ») renseigne sur la parité du résultat. Il vaut 1 si ce dernier est pair.
  • AF (« Auxiliary Carry Flag ») est peu utilisé.
  • ZF (« Zero Flag ») passe à 1 si le résultat d'une opération est égal à zéro.
  • SF (« Sign Flag ») passe à 1 si le résultat d'une opération sur des nombres signés est négatif.
  • TF (« Trap Flag ») est utilisé pour le « débogage » d'un programme. S'il vaut 1, une routine spéciale du débogueur est appelée après l'exécution de chaque instruction par le processeur.
  • IF (« Interrupt Flag ») sert à empêcher les appels d'interruptions lorsqu'il est positionné à 1. Cependant, toutes les interruptions ne sont pas « masquables ».
  • DF (« Direction Flag ») est utilisé pour les opérations sur les chaînes de caractères. S'il vaut 1, celles-ci seront parcourues dans le sens des adresses décroissantes, sinon les adresses seront croissantes.
  • OF (« Overflow Flag ») indique qu'un débordement s'est produit, c'est-à-dire que la capacité de stockage a été dépassée. Il est utile en arithmétique signée. Avec des nombres non signés, il faut utiliser ZF et SF.

Remarque : Les notations CF, PF, AF, etc. ne sont pas reconnues par l'assembleur. Pour utiliser les flags, il existe des instructions spécifiques que nous décrirons plus tard.

II-D. LES INTERRUPTIONS

II-D-1. Introduction

Le microprocesseur ne peut exécuter qu'une seule instruction à la fois. Pour connaître son adresse, il utilise le couple de registres CS:IP dont la valeur est incrémentée automatiquement. Par conséquent, le code du programme courant est exécuté de manière linéaire.

Imaginons cependant qu'un événement extérieur demande l'attention de l'ordinateur, par exemple la pression d'une touche du clavier. La machine doit pouvoir réagir immédiatement, sans attendre que le programme en cours d'exécution se termine. Pour cela, elle interrompt ce dernier pendant un bref instant, le temps de traiter l'événement survenu puis rend le contrôle au programme interrompu.

Une interruption n'est rien d'autre que l'appel d'une routine spéciale présente en mémoire appelée ISR (« Interrupt Service Routine »).

Les interruptions se divisent en trois catégories :

  • les interruptions électroniques, par exemple : le clavier ;
  • les interruptions du BIOS, par exemple : l'accès aux disques ;
  • les interruptions du DOS, par exemple : l'accès aux systèmes de fichiers.

Comment sont-elles déclenchées ?

Une interruption peut être déclenchée par votre matériel. C'est ce qui arrive lorsque vous appuyez sur une touche du clavier. Aucun logiciel n'intervient et le contrôle est passé directement à la routine qui gère le clavier : ce sont les interruptions matérielles.

Les interruptions logicielles sont quant à elles appelées par des instructions en langage machine au sein d'un programme. Leur importance est capitale. Rappelez-vous que contrairement au PASCAL ou au C, l'assembleur ne dispose pas de fonction préprogrammée. Chaque instruction doit être directement traduisible en langage machine.

Mais alors, comment fait-on pour écrire une chaîne de caractères à l'écran ? Ou bien pour lire un caractère entré au clavier ?

Eh bien on le fait de la même façon que le DOS lui-même ! On déclenche les interruptions appropriées à l'aide de l'instruction « INT » du langage machine. C'est donc une routine du DOS (ou parfois du BIOS) qui fera tout le travail. Les paramètres (ou leurs adresses) sont passés dans les registres.

Voici un petit exemple en assembleur qui écrit la lettre ‘A' à l'écran :

Image non disponible

Examinons-le ligne par ligne :

  • l'instruction « MOV DL, ‘A' » demande au processeur de mettre dans le registre DL le code ASCII de la lettre ‘A', c'est-à-dire 65, ou 41h ;
  • « MOV AH, 02 » : mettre le nombre 2 dans AH ;
  • enfin, la dernière instruction appelle l'interruption numéro 21h. Il existe 256 interruptions. Toutes sont notées en base hexadécimale.

Explications :

Comme vous aurez très vite l'occasion de vous en rendre compte, l'interruption 21h est l'interruption du DOS par excellence. Elle permet d'appeler de nombreuses fonctions très diverses. Pour cela, il suffit de mentionner leur numéro dans le registre AH.

Il est très difficile de mémoriser le rôle de chaque interruption, et a fortiori de chaque fonction ou sous-fonction, d'autant plus qu'elles sont désignées par des numéros hexadécimaux et qu'elles attendent des paramètres dans des registres précis. C'est pourquoi tout programmeur se doit d'avoir à sa disposition une liste des interruptions pour travailler. Celle de Ralph Brown est très connue, et vous la trouverez sur l'Internet.

Revenons à notre exemple. La fonction numéro 2 de l'interruption 21h sert à écrire un caractère à l'écran. Il faut pour cela écrire le code ASCII du caractère dans le registre DL et bien sûr placer le nombre 2 dans AH.

Une fois que AH et DL ont été ajustés, l'interruption 21h peut être appelée à l'aide de l'instruction INT.

D'autres interruptions ne remplissent qu'une seule tâche. Vous n'avez donc pas besoin de mettre un numéro de fonction dans AH. L'appel de l'interruption suffit.

II-D-2. La table des vecteurs d'interruptions

À chaque appel d'interruption, l'ordinateur doit pouvoir trouver l'adresse de l'ISR correspondante. Pour cela, il dispose de la table des vecteurs d'interruptions (TVI, ou IVT : « Interrupt Vector Table »). Cette table est implantée à l'adresse 0000:0000 c'est-à-dire au début de la RAM.

La table est organisée comme suit :
Adresse | Taille (octets) | Valeur
0000:0000 | 2 | Adresse d'offset de l'interruption numéro 0
0000:0002 | 2 | Adresse de segment de l'interruption numéro 0
0000:0004 | 2 | Adresse d'offset de l'interruption numéro 1
0000:0006 | 2 | Adresse de segment de l'interruption numéro 1
0000:0008 | 2 | Adresse d'offset de l'interruption numéro 2
0000:000A | 2 | Adresse de segment de l'interruption numéro 2
0000:000C | 2 | Adresse d'offset de l'interruption numéro 3
0000:000E | 2 | Adresse de segment de l'interruption numéro 3
etc. etc. etc.
0000:03FC | 2 | Adresse d'offset de l'interruption numéro 255
0000:03FE | 2 | Adresse de segment de l'interruption numéro 255

La raison pour laquelle les offsets précèdent les adresses de segment vient du codage en « little endian » utilisé par INTEL. Les processeurs de cette marque (contrairement à la plupart des autres, qui travaillent en « big endian ») ont une représentation des données en mémoire aussi curieuse qu'insupportable pour le programmeur : ils placent le poids fort après le poids faible. Ainsi, le mot 4A28h sera codé 284Ah. Puisque l'adresse de l'ISR tient sur 32 bits (16 + 16), elle est représentée comme un double-mot (« dword »), donc l'adresse d'offset, qui est le mot de poids faible, se trouve au début. Eh oui, c'est pénible, mais il faudra s'y faire !

Remarque : pour calculer l'adresse de l'entrée dans la TVI correspondant à l'interruption numéro X, il suffit de multiplier X par 4.

Exemple : l'adresse de l'ISR numéro 21h est stockée à 0000:0084.

II-D-3. Sauvegarde de l'état des registres lors de l'appel

Un appel d'interruption obéit à certaines règles, car il est indispensable, une fois l'ISR exécutée, que le programme interrompu retrouve les registres dans le même état qu'ils étaient auparavant. Tout doit se passer comme si l'interruption n'avait jamais eu lieu. C'est pourquoi avant de faire un saut à l'ISR pointée par l'entrée correspondante dans la TVI, l'ordinateur sauvegarde tous les registres sur la pile courante. Il restaurera leur contenu avant de rendre le contrôle. Cette procédure est automatique. Il n'incombe pas au programmeur de prendre toutes ces précautions.


III

III. DEUXIÈME PARTIE

Cette partie a pour but de vous présenter l'architecture du langage assembleur à travers de courts exemples. Ne vous inquiétez pas si vous ne comprenez pas parfaitement certaines instructions : toutes seront détaillées dans la troisième partie. L'essentiel est que vous compreniez grossièrement ce qu'on fait et pourquoi on le fait . Les explications qui accompagnent ces exemples devraient y suffire. Vous pourrez ensuite apprendre le langage en lui-même en consultant la partie suivante.

Remarques pratiques préliminaires

  • Les mots qui sont imprimés en italiques sont ceux qui ne font pas partie du langage en lui-même. Ils sont choisis par le programmeur.
  • Tous les programmes présentés ci-dessous suivent la syntaxe du compilateur TASM. Il existe de très légères différences d'un compilateur à l'autre.
  • Le code peut être écrit en majuscules ou en minuscules. Chaque mot est séparé des autres par des espaces ou des tabulations.
  • Les commentaires sont précédés du signe ‘;'.
  • Les nombres doivent toujours commencer par un chiffre (entre 0 et 9), même les nombres hexadécimaux. Il faut donc écrire 0F2Ah et non pas F2Ah. Par contre, l'écriture 5CFh est correcte.
  • Les apostrophes et les guillemets sont équivalents. Il est cependant plus commode de réserver l'usage des guillemets aux chaînes de plusieurs caractères et celui des apostrophes aux caractères isolés.
  • Vous pouvez taper vos programmes avec l'éditeur de texte du DOS (EDIT.COM). Donnez-leur l'extension ‘.asm'.
  • La compilation se fait en tapant “TASM /m9 MONPROG” si votre source s'appelle ‘MONPROG.ASM'.

    Le paramètre ‘/m9', facultatif, indique au compilateur qu'il devra effectuer 9 passes, c'est-à-dire qu'il examinera 9 fois le code source. Comme chaque examen se fait de manière linéaire, le compilateur trouve parfois des instructions qui font référence à des labels placés plus loin dans le programme. Il manque donc d'informations, mais il continue son examen jusqu'à la fin. Quand il a terminé, il recommence tout depuis le début pour résoudre les problèmes qui s'étaient posés.

    La compilation crée un fichier objet (‘.obj'). Pour obtenir un fichier EXE, tapez ”TLINK MONPROG”. Pour un fichier COM, tapez “TLINK /tdc MONPROG”.

    Après l'édition des liens (le « linkage »), vous pouvez supprimer les fichiers MONPROG.obj' et ‘MONPROG.map'.

  • Il est possible de créer un fichier BAT qui s'occupe de toutes ces étapes. Ouvrez l'éditeur EDIT du DOS et tapez un programme tel que celui-ci :

    Enregistrez-le et nommez-le MAKE.BAT. Vous pouvez compiler et linker en tapant “MAKE MONPROG”. Pour compiler des fichiers COM, ajoutez le paramètre '/tdc' à la commande TLINK.

    Sélectionnez
    @ECHO OFF
TASM /m9 %1.asm
IF NOT EXIST %1.obj GOTO FIN
TLINK %1.obj REM : ajouter ici /tdc pour obtenir un fichier COM
ERASE %1.map
ERASE %1.obj
:FIN
  • Pour pouvoir lancer TASM et TLINK quel que soit le dossier courant, changez la variable PATH ainsi :

    PATH = %path%;c:\MonChemin\DossierTASM

    Vous pouvez inclure cette ligne à la fin de votre fichier AUTOEXEC.BAT afin qu'elle soit exécutée à chaque démarrage.

  • Vous pouvez déboguer vos programmes (par exemple les exécuter instruction par instruction en observant les changements induits dans la RAM et dans les registres…) avec le logiciel Turbo Debugger 16/32 bits qui est très pratique et très performant. À défaut, vous pouvez vous rabattre sur l'archaïque DEBUG.COM (il se trouve dans votre dossier de commandes DOS). Mais alors bon courage !

Le seul travail du compilateur (et du linkeur) est de convertir chacune de vos instructions en son équivalent en langage machine. Le programme compilé présentera donc exactement la même structure et la même linéarité que votre code source .

III-A. PREMIER EXEMPLE : LES FICHIERS COM

Voici un petit programme COM qui écrit le message « Bonjour, monde ! » à l'écran.

Image non disponible

Ce code source commence par des directives. Une directive est une information que le programmeur fournit au compilateur. Elle n'est pas transformée en une instruction en langage machine. Elle n'ajoute donc aucun octet au programme compilé.

La directive “.386“ indique au compilateur que le programme est destiné à tourner sur des processeurs INTEL de modèle 386 (ou supérieur). Cela nous autorise à utiliser certaines instructions qui ne sont pas disponibles sur les modèles antérieurs, comme PUSHA ou POPA. Dans cet exemple, cette directive aurait très bien pu être omise.

La ligne

code segment use16

sert à déclarer un segment que l'on appelle “code”. On aurait tout aussi bien pu le nommer “marteau” ou “voiture”. Ce sera le segment de notre programme. N'oubliez pas qu'un fichier COM ne peut comporter qu'un seul segment. Cette ligne ne sera pas compilée : elle ne sert qu'à indiquer au compilateur le début d'un segment.

Le mot “use16” indique que les adresses de segment et d'offset sont codées sur 16 bits et non sur 8 bits. Vous devez systématiquement l'écrire.

La directive

assume cs:code, ds:code, ss:code

informe le compilateur que tout au long du programme, CS, DS et SS pointeront de façon privilégiée vers le segment “code”, ce qui est évident d'ailleurs puisque c'est le seul segment… Vous comprendrez le sens exact de “assume” dans la troisième partie du cours.

Enfin, les mots

org 100h

signifient qu'il faudra ajouter 100h (soit 256) à tous les offsets. Pourquoi ? Souvenez-vous de la structure d'un programme COM en mémoire. Si vous n'écrivez pas cette ligne, TASM considérera que le programme débute à l'offset 0000. Or, lors de l'exécution, le DOS le chargera après le PSP, c'est-à-dire à l'adresse 100h. C'est pourquoi il est nécessaire de recalculer les offsets : l'offset 0000 deviendra 0100. Cette directive est en quelque sorte le trait caractéristique des fichiers COM.

Nous en arrivons au programme proprement dit. Il commence par un label :

debut :

Lui non plus n'est pas compilé. Il ne sert qu'à représenter l'adresse de l'instruction qui le suit, c'est-à-dire :

mov ah, 09h

Cette ligne demande au processeur de charger la valeur 9 dans le registre AH. C'est le numéro de la fonction de l'interruption 21h qui écrit une chaîne de caractères à l'écran. L'offset de cette chaîne est attendu dans DX. D'où la ligne suivante :

mov dx, offset message

Le mot-clé “offset” sert à extraire l'offset du label “message” qui représente quant à lui l'adresse du message à écrire (il contient donc une adresse de segment ET un offset). L'adresse de segment de la chaîne doit être transmise dans DS. Mais il est inutile de changer ce registre, car il pointe déjà vers notre segment.

Il nous reste à appeler l'interruption 21h :

int 21h

et à rendre la main au DOS :

ret

Lorsque nous aborderons les procédures, vous comprendrez mieux le sens de ce mot et ce qu'il fait exactement. Pour l'instant, sachez simplement que seul un fichier COM peut se terminer avec cette instruction.

Nous arrivons à la ligne :

message db “Bonjour, monde !”, ‘$'

Il s'agit d'une définition de données. Le mot “db” (« define byte ») signifie que le compilateur devra écrire les octets qui suivent tels qu'ils sont dans notre code source. Il va donc écrire le code ASCII du ‘B', puis celui du ‘o', etc. Il terminera en écrivant le code ASCII du signe ‘$'. C'est ainsi que la fonction 9 de l'interruption 21h reconnaît la fin de la chaîne à écrire. Si vous oubliez ce signe, elle écrira tous les octets de la RAM jusqu'à ce qu'elle tombe par hasard sur lui.

Le mot “message” placé en début de ligne est un label de données. Il représente l'adresse du code ASCII du ‘B'. Remarquez qu'il n'y a pas de caractère ‘:' après un label de données.

La ligne

code ends

indique la fin du segment « code », et enfin

end debut

informe le compilateur que le fichier est fini, tout comme le “END.” du PASCAL. Le nom du label « debut » est mentionné : ce sera le point d'entrée de notre programme. C'est vers lui que pointera CS:IP avant l'exécution.

Remarque : vous n'êtes pas tenu de rendre aux registres la valeur qu'ils avaient au début de votre programme. De toute façon, avant de charger un programme, le DOS sauvegarde le contenu de tous les registres puis met le contenu des registres généraux (ainsi que SI, DI et BP) à zéro. Il les restaurera quand vous lui rendrez la main.

III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE

Le programme suivant est un programme EXE qui fait exactement la même chose que l'exemple précédent.

Image non disponible

Examinons-le !

Tout d'abord, la directive

org 100h

a disparu.

En effet, lors de l'exécution, CS pointera vers notre segment de code et non pas vers le PSP. Il est donc inutile de décaler les offsets de 256.

Remarquons que notre programme dispose de deux segments supplémentaires :

  • le segment « data » est destiné à contenir les données, c'est-à-dire les variables ;
  • le segment « pile » sera notre segment de pile.

Notre directive « assume » devient donc :

assume cs:code, ds:data, ss:pile

Ainsi, le compilateur est informé que DS pointera vers le segment « data » et SS vers « pile ».

Les deux lignes suivantes,

mov ax, data

mov ds, ax

servent à initialiser le registre DS. Celui-ci pointe vers le PSP au début du programme, mais nous voulons le faire pointer vers notre segment de données appelé « data ». Cela est nécessaire puisque la fonction 9 de l'interruption 21h attend l'adresse de la chaîne dans le couple DS:DX et que notre message se trouve dans le segment de données.

La première instruction charge l'adresse du segment « data » dans AX. La seconde transfère cette valeur de AX dans DS.

Mais pourquoi diable utiliser AX comme intermédiaire ? Après tout, on pourrait écrire :

mov ds, data

Eh bien non ! Pour la simple raison que DS est un registre de segment et qu'en tant que tel on ne peut pas lui charger de valeur immédiate.

On appelle « valeur immédiate » toute constante tapée directement dans l'instruction elle-même.

Exemples de chargement de valeurs immédiates :

mov ax, 135 ;charge 135 dans AX

mov bx, offset message ;charge l'offset de message dans BX

mov bx, offset fin - offset debut ;charge le nombre d'octets entre fin et debut dans BX

mov es, 10 ;instruction illicite, car ES est un registre de segment !

Remarque : une autre possibilité aurait été d'écrire : “PUSH AX” (empiler AX) puis “POP DS” (dépiler le dernier nombre empilé et le placer dans DS).

Les trois lignes suivantes :

mov ah, 09h

mov dx, offset message

int 21h

n'ont pas changé.

Il nous faut également terminer le programme par un appel de la fonction 4ch de l'interruption 21h. C'est ainsi que se terminent les programmes EXE.

mov ah, 4ch

int 21h

Remarque : on aurait également pu écrire :

mov ax, 4c00h

int 21h

La seule différence est que AL est mis à zéro, ce qui indique au programme à qui on rend le contrôle (ici le DOS) que notre programme s'est terminé normalement. Les fichiers COM peuvent également utiliser la fonction 4ch.

Le segment de code se termine :

code ends

et le segment de données commence à sa suite :

data segment use16

message db “Bonjour, monde !”, ‘$'

data ends

Il nous reste à écrire le segment de pile. Dans ce programme, il n'était pas absolument indispensable de le séparer du segment de code. Mais c'est une bonne habitude de le faire.

pile segment stack

remplissage db 256 DUP (?)

pile ends

Le mot-clé « stack » indique que ce segment est le segment de pile.

Les mots « db 256 DUP (?) » déclarent 256 octets non initialisés. C'est la « matière » de notre pile.

Sachez que tout appel d'interruption se traduit par l'empilage des flags et de CS:IP. Il est donc indispensable d'avoir une pile, même si celle-ci peut éventuellement partager le même segment que le code, comme dans un fichier COM. Mais dans un programme EXE, il vaut mieux réserver un segment à la pile. Les 256 octets que nous déclarons ici indiquent seulement que la pile contient 256 octets. Ainsi, au début de l'exécution, SS:SP pointera vers la fin de ces octets. Nous verrons ce que signifie le point d'interrogation dans la troisième partie.

La fin du fichier et le point d'entrée sont signalés par :

end debut

Et voilà ! Vous avez découvert l'allure d'un programme en assembleur. Nous pouvons donc à présent nous pencher sur l'étude du langage.


IV

IV. TROISIÈME PARTIE

IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE

IV-A-1. Les définitions de données

Les mots “db” (« define byte »), “dw” (« define word »), ”dd” (« define double word ») permettent de déclarer et d'initialiser une variable. Il faut bien comprendre que leur seule action est d'écrire les données dans l'exécutable à l'endroit même où elles se trouvent dans le code source.

Pour accéder à ces données (appelées « variables »), il suffit de connaître leur adresse. Pour cela, on peut les faire précéder d'un label de données.

Exemple :

TOTO dw 1982

Dans la ligne précédente, TOTO représente l'adresse du nombre 1982 qui est défini juste après. Comme ce nombre est codé sur deux octets (word = 16 bits), c'est le premier octet qui est adressé par TOTO.

Le compilateur se contentera d'écrire le nombre 1982 à l'endroit où se trouve la déclaration.

Remarque : en C, on écrirait ceci :

int TOTO = 1982;

En BASIC ou en PASCAL, cette définition n'a pas d'équivalent, puisque les variables ne peuvent être initialisées lors de leur déclaration. Il est donc obligatoire d'ajouter une ligne de code pour le faire.

Dans la partie précédente, nous avons eu à définir le message qui serait affiché à l'écran. Voici la ligne que nous avons écrite :

message db “Bonjour, monde !”, ‘$'

Lorsqu'il rencontre une chaîne de caractères, le compilateur écrit les codes ASCII de tous les caractères, les uns à la suite des autres.

Voici une autre manière d'écrire cette définition de données. Le résultat (i.e. le programme compilé) est EXACTEMENT LE MÊME :

message db 42h, 111, ‘nj'

db ‘our, mo'

db ‘nde !$'

Pour définir plusieurs fois à la suite les mêmes données, on utilise “DUP” (« duplicate ») de la manière suivante :

TOTO db 100 dup(“TOTO EST BEAU”)

Cela revient à écrire :

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

;… etc. (100 fois) …

Il est fréquent que de nombreuses données n'aient pas besoin d'être initialisées à une valeur précise. Dans ce cas, on les regroupe à la fin du programme et on les remplace par le caractère ‘?'. Les adresses des labels seront calculées de la même façon, mais aucune donnée ne sera écrite dans le fichier (et a fortiori dans la RAM). On dit que l'on met ses variables sur le « tas » (« heap » en anglais). La seule différence avec une définition classique est qu'au début de l'exécution, nos variables n'auront pas de valeur définie. Leurs valeurs seront « aléatoires » en ce sens qu'on ne peut les connaître au moment de la compilation.

Exemple :

Image non disponible

Remarque : Si elles ne sont pas regroupées en fin de programme, le compilateur sera obligé d'écrire les données dans le fichier afin de ne pas fausser les adresses des variables (ou du code) qui suivent. Il remplira alors les points d'interrogation avec des zéros.

Pour indiquer qu'un nombre est noté en base hexadécimale, on lui ajoute la lettre ‘h'. La lettre ‘b' signifie que le nombre est codé en binaire, la lettre 'o' en octal et la lettre ‘d' en base décimale (base par défaut).

IV-A-2. L'adressage

IV-A-2-a. L'adressage immédiat

On appelle « adressage immédiat » l'adressage qui ne fait intervenir que des constantes.

Considérons cette partie de programme qui stocke le nombre 125h dans une variable appelée TOTO, lui ajoute 15 puis charge le résultat dans AX.

Image non disponible

La ligne

mov word ptr ds:[TOTO], 125h

charge le nombre 125h dans le mot de la RAM adressé par DS et l'offset de “TOTO”. La ligne suivante ajoute 15 au contenu de ce mot.

L'expression “word ptr” devant l'adresse, obligatoire ici, indique la taille de la variable dans laquelle doit être stocké le nombre 125h.

Si on avait mis “dword ptr”, ce nombre aurait été codé sur 32 bits (00000125h) au lieu de 16 bits (0125h) : on aurait donc écrasé les deux octets qui suivent la variable. Si on avait mis “byte ptr”, la compilation aurait été impossible, car un octet ne peut contenir un nombre supérieur à FFh.

Le compilateur n'a en effet aucun moyen de connaître cette taille. La variable “TOTO” n'a pas de taille (malgré le mot “dw”) : ce n'est en fait qu'un pointeur vers le premier octet du word qu'elle représente.

En revanche, la troisième instruction ne fait pas apparaître l'expression “word ptr”. Cette fois-ci elle est facultative du fait que la destination (AX = 16 bits) impose la taille.

On peut évidemment remplacer “TOTO” par une constante numérique :

mov word ptr ds:[004Ch], 125h

Le mot 125h sera alors écrit à l'adresse DS:4C.

Une question se pose à présent : que se passe-t-il si le programmeur n'écrit pas le registre de segment dans l'adresse ?

C'est là qu'intervient la directive “assume”. Le compilateur va devoir trouver lui-même quel registre l'utilisateur a voulu sous-entendre.

Si on a utilisé un label, alors le segment sera celui dans lequel est déclaré le label. Mais le compilateur veut un REGISTRE de segment. Il va donc prendre celui qui est censé pointer vers le bon segment et pour le savoir, il examine la directive assume.

Voilà pourquoi cette dernière peut nous épargner d'écrire pour chaque variable l'expression “ds:”.

Comprenez bien que cette directive ne sert à rien d'autre qu'à cela et qu'en aucune façon elle ne force les registres de segment à prendre quelque valeur que ce soit.

IV-A-2-b. L'adressage indexé et/ou basé

Comme nous l'avons déjà expliqué, il est possible d'utiliser des registres de base ou d'index pour adresser un octet.

On appelle « base » les registres BX et BP et « index » les registres SI et DI. La différence est que la base est censée être fixe tandis que l'index varie automatiquement lorsqu'on utilise certaines opérations, telles que MOVSB.

Remarque : BX, BP, DI et SI peuvent tous être utilisés comme des registres généraux.

Voici les adressages possibles :

  • Constante seule (= adressage immédiat) :

Exemples :

MOV AH, byte ptr [TOTO]

MOV BX, word ptr ds:[1045h]

MOV ES:[10 + TOTO x 2], BL

  • Base seule :

Exemple :

MOV dword ptr ds:[BP], 15

  • Index seul :

Exemple :

MOV dword ptr es:[DI], 142

  • Base + Constante :

Exemples :

MOV byte ptr ds:[BP + 1], 12

MOV ds:[BX + (TOTO - BOBO)/10], AX

  • Index + Constante :

Exemples :

MOV CX, [TOTO + DI]

MOV AL, ds:[1 + SI]

  • Base + Index :

Exemple :

MOV ds:[BP + SI], AH

  • Base + Index + Constante :

Exemple :

MOV AX, word ptr [TOTO + BX + DI + 1]

Remarque : si la constante n'est pas un label, il est parfois impératif de spécifier le registre de segment ! Tout dépend du contexte…

IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS

IV-B-1. Les sauts inconditionnels

Ce paragraphe répond à la question : « Mais comment fait-on un GOTO en assembleur ? ».

L'instruction équivalente est JMP qui signifie « jump ».

La syntaxe est

JMP MonLabel

La référence à l'étiquette MonLabel sera remplacée lors de la compilation par la distance (signée) en octets qui sépare l'instruction qui suit immédiatement le JMP de l'instruction adressée par MonLabel. En pratique, le JMP s'utilise exactement comme un GOTO en BASIC.

Cette instruction permet de faire un saut inconditionnel : le programme fera ce saut quoi qu'il arrive.

Exemple :

Image non disponible

Le mot “short” ajouté après “jmp” indique au compilateur que le label “COUCOU” se trouve à une distance (signée) qui peut être stockée sur un seul octet. Le compilateur ne le sait pas encore lorsqu'il essaie de compiler l'instruction de saut, car le label se trouve en deçà de cette instruction. C'est pourquoi il prévoit deux octets pour écrire le saut, au cas où l'adresse d'arrivée est éloignée de plus de 128 octets. Lorsqu'il effectue une deuxième « passe », s'il s'aperçoit qu'un seul octet aurait suffi il remplit alors l'octet inutile avec l'instruction NOP (« No Operation ») qui ne fait rien du tout. Le programme marchera parfaitement (encore heureux !), mais cela gaspille un octet. Le programmeur a la possibilité d'aider le compilateur en ajoutant le mot “short”, ainsi un seul octet est prévu pour la distance de saut. Si le label est placé plus haut que l'instruction de saut, il est inutile de l'écrire.

Remarque : Ce mot peut également être utilisé avec les instructions de saut conditionnel que nous étudierons plus loin.

IV-B-1-a. Les procédures

IV-B-1-a-i. appels de procédures
Ceux qui ont déjà programmé en BASIC connaissent les instructions GOSUB et RETURN. Leurs équivalents en assembleur sont CALL et RET.
  • L'instruction CALL sert à appeler une procédure :

Syntaxe :

CALL MonLabel

Action : empile l'offset de l'instruction suivante puis fait un simple saut à l'adresse représentée par MonLabel.

  • RET (ou RETN) permet de retourner à l'instruction qui suit immédiatement le CALL :

Syntaxe :

RET

Action : dépile l'adresse de retour et la met dans IP. Le programme continue donc à l'adresse qui suit le CALL.

Remarque : Rappelez-vous qu'il est possible de terminer un programme COM avec l'instruction RET. Puisque le DOS empile un zéro de deux octets au chargement du programme, IP prendra la valeur 0000 lorsque le processeur exécutera cette instruction : il pointera donc vers le début du PSP. Or le PSP commence toujours par les deux octets suivants : CDh 20h (ce qui s'écrit INT 20h en assembleur). L'interruption 20h, qui permet de terminer un programme COM, sera donc appelée et le contrôle sera rendu au DOS.

Voici un exemple d'utilisation des procédures aussi simple que possible : ce programme COM appelle 12 fois une procédure qui écrit un message à l'écran et rend la main au DOS. Il n'est d'aucune utilité et n'est pas optimisé du tout.

Image non disponible

Remarque : Les codes ASCII 10 et 13 représentent respectivement la fin de ligne et le retour chariot. Grâce à eux, on revient à la ligne chaque fois qu'on a écrit le message.

Ce programme fonctionne parfaitement. Toutefois, les conventions d'écriture veulent que les procédures soient écrites comme suit :

Image non disponible

Le programme compilé est toujours le même, mais le code est plus lisible puisqu'on distingue les procédures des simples labels. Le mot “near” signifie que l'adresse de cette procédure est réduite à un offset. Il n'y a pas d'adresse de segment. La procédure ne pourra donc être appelée que de l'intérieur même du segment.

Le mot qui a le sens contraire de “near” est “far”. Les « call far » sont assez peu utilisés.

IV-B-1-a-ii. le passage des paramètres
Il existe deux moyens de passer des paramètres à une procédure : les registres et la pile.
  • Le passage par registres :

Il consiste à transmettre les paramètres dans des registres convenus à l'avance, par exemple AX et BX. Les deux gros avantages de cette méthode sont sa simplicité et la vitesse d'exécution. L'inconvénient majeur est le nombre limité de registres, d'autant plus qu'au moment de l'appel, certains d'entre eux ne seront peut-être pas disponibles.

  • Le passage par la pile :

C'est la méthode qu'utilisent les langages de haut niveau tels que le C ou le PASCAL. Avant l'appel, on empile les paramètres un à un. La procédure appelée se chargera de les lire.

Pour cela on utilise le registre BP de la manière suivante : on transfère la valeur de SP dans BP à l'aide de l'instruction “MOV BP, SP”. L'adresse de retour (qui sera dépilée quand le processeur rencontrera l'instruction “RET”) se trouve à l'adresse SS:[BP], le dernier paramètre est adressé par SS:[BP + 2], l'avant-dernier par SS:[BP + 4], etc. On peut ainsi lire n'importe quel paramètre sans le dépiler.

Attention cependant ! À la fin de la procédure, il faudra tout de même rendre à SP la valeur qu'il avait avant l'empilage des paramètres. Pour cela, on fait suivre l'instruction RET du nombre de paramètres, multiplié par 2 (car chaque paramètre comporte deux octets).

Exemple :

Image non disponible

Soyez toujours très vigilant avec les appels de procédures : pensez que l'adresse de retour sera dépilée lorsque le programme rencontrera l'instruction RET. Il serait donc suicidaire de laisser une donnée empilée avant d'appeler cette instruction.

IV-B-1-b. Les macros

Étant donné que certaines instructions se répètent constamment dans un programme, l'écriture de macrofonctions (ou macros) est un moyen pratique de rendre votre code source plus lisible.

Il est possible de choisir pour certaines suites d'instructions un nom qui les représente. Lorsque le compilateur (en fait, le préprocesseur) rencontrera ce nom dans votre code source, il le remplacera par les lignes de code qu'il désigne. Ces lignes forment une « macro ».

Les macros, à la différence des procédures, n'ont aucune signification pour la machine. Seul le compilateur comprend leur signification. Elles ne sont qu'un artifice mis à la disposition du programmeur pour clarifier son programme. Lorsque le compilateur rencontre le nom d'une macro dans votre code, il le remplace par le code de la macro. Tout se passe exactement comme si vous aviez tapé vous-même ce code à la place du nom de la macro.

Ainsi, si vous appelez quinze fois une macro dans votre programme, le compilateur écrira quinze fois le code de cette macro. C'est toute la différence avec les procédures qui ne sont écrites qu'une seule fois, mais peuvent être appelées aussi souvent qu'on veut à l'aide d'un CALL.

Voici comment écrire une macro : l'exemple suivant sert à écrire un message à l'écran.

Image non disponible

Le code précédent peut être écrit n'importe où dans votre programme, à condition qu'il se trouve avant tout appel de cette macro. Afin d'éviter les ennuis, il est fortement conseillé de réunir vos macros au début du code, avant toute autre ligne. De toute façon, il ne sera pas compilé à l'endroit où vous l'avez écrit, mais aux endroits où se trouvent les appels de macros.

Le mot “text?” est un « paramètre ». Le point d'interrogation n'est pas requis ; nous l'avons mis pour indiquer qu'il s'agit d'un paramètre et non d'une variable. Une macro peut utiliser plusieurs paramètres séparés par des virgules.

Pour appeler cette macro, il vous suffit d'écrire la ligne

ecrit_texte “Coucou ! Ceci est un essai !”

Le compilateur se chargera alors de la remplacer par les instructions comprises entre la première et la dernière ligne de cet exemple, en prenant le soin de remplacer le mot “text?” par le message fourni en paramètre.

En assembleur, chaque label doit avoir un nom unique. Or, si une macro est appelée plusieurs fois, les mêmes noms seront utilisés. Il faut donc la plupart du temps inclure la directive LOCAL qui forcera le compilateur à changer le nom des labels à chaque appel de la macro. Attention : cette directive doit suivre immédiatement la déclaration de la macro.

Supposons à présent que l'on veuille écrire à l'écran le message « Je suis bien content » et revenir à la ligne à l'aide de notre macro ecrit_texte.

La syntaxe suivante :

ecrit_texte “Coucou ! Ceci est un essai !”, 10, 13

est incorrecte, car le compilateur croirait que l'on a écrit trois paramètres ! Il faut alors entourer notre unique paramètre par les signes ‘<' et ‘>' :

ecrit_texte <”Coucou ! Ceci est un essai !”, 10, 13>

IV-B-1-c. La directive EQU

La directive EQU a un rôle voisin de celui des macros. Elle permet de remplacer un simple mot par d'autres plus complexes. Son intérêt est qu'elle peut être invoquée en plein milieu d'une ligne.

Quelques exemples :

Longueur EQU (fin - debut)

Message EQU “Bonjour messieurs ! Comment allez-vous ?”, ‘$'

Version EQU 2

Quitter EQU ret

Quitter2 EQU int 20h

Mettre_dans_AH EQU mov ah,

Interruption_21h EQU int 21h

Un programme qui contient de telles directives peut se terminer ainsi :

Mettre_dans_AH 4Ch

Interruption_21h

Ou ainsi (si c'est un COM) :

Quitter2

IV-B-1-d. L'inclusion de fichiers

Il est possible d'accéder à des procédures, des macros ou des définitions EQU qui se trouvent dans d'autres fichiers. Cela permet de se constituer des bibliothèques de macros ou de procédures que l'on peut réutiliser d'un programme à l'autre.

Pour inclure le fichier TOTO.LIB, écrivez au début de votre code source :

Image non disponible

La condition “if1” indique au compilateur que l'inclusion ne doit s'effectuer que lors de la première passe.

Le fichier TOTO.LIB est un fichier texte tout à fait banal qui contient des lignes de code en assembleur.

IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE

Remarques préliminaires :
  • Le principe du langage assembleur est de remplacer chaque opcode hexadécimal par un mot facile à retenir. Ce mot est appelé mnémonique. Par exemple, “INT” est le mnémonique associé à l'opcode CDh. Chaque fois que le compilateur rencontrera ce mot, il le remplacera par l'octet CDh et écrira ensuite l'opérande (ici : le numéro de l'interruption) en hexadécimal.
  • Cette liste récapitule les instructions que nous connaissons déjà et en présente de nouvelles. Elle n'est pas exhaustive, mais vous sera amplement suffisante pour la plupart de vos programmes.
  • Certaines instructions, comme PUSHA, ne sont disponibles que pour des modèles de processeurs plus évolués que le 8086, par exemple le 286. N'oubliez pas la directive .386 si vous les utilisez.

IV-C-1. L'instruction NOP (« No Operation »)

Syntaxe : NOP

Description : Ne fait rien ! Mais alors RIEN ! Que dalle ! Niet !

IV-C-2. L'instruction MOV (« Move »)

Syntaxe : MOV Destination, Source

Description : Copie le contenu de Source dans Destination.

Mouvements autorisés :

MOV Registre général, Registre quelconque

MOV Mémoire, Registre quelconque

MOV Registre général, Mémoire

MOV Registre général, Constante

MOV Mémoire, Constante

MOV Registre de segment, Registre général

Remarques : Source et Destination doivent avoir la même taille. On ne peut charger dans un registre de segment que le contenu d'un registre général (SI, DI et BP sont considérés ici comme des registres généraux).

Exemples :

MOV AX, 5

MOV ES, DX

MOV AL, [Variable1] ; Copie un octet, car AL contient 8 bits

MOV [Variable2], DS ; Copie un word car DS contient 16 bits

MOV word ptr [Variable3], 12 ; Ici, on spécifie que la variable est un word

IV-C-3. L'instruction XCHG (« Exchange »)

Syntaxe : XCHG Destination, Source

Description : Échange les contenus de Source et de Destination.

Mouvements autorisés :

XCHG Registre général, Registre général

XCHG Registre général, Mémoire

XCHG Mémoire, Registre général

IV-C-4. L'instruction JMP (« Jump »)

Syntaxe : JMP MonLabel

Description : Saute à l'instruction pointée par MonLabel.

IV-C-5. L'opérateur CMP (« Compare »)

Syntaxe : CMP Destination, Source

Description : Cet opérateur sert à comparer deux nombres : Source et Destination. C'est le registre des indicateurs qui contient les résultats de la comparaison. Ni Source ni Destination ne sont modifiés.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : Cet opérateur effectue en fait une soustraction, mais contrairement à SUB, le résultat n'est pas sauvegardé.

Le programme doit pouvoir réagir en fonction des résultats de la comparaison. Pour cela, on utilise les sauts conditionnels (voir ci-dessous).

IV-C-6. Les instructions de saut conditionnel

Les sauts conditionnels sont terriblement importants, car ils permettent au programme de faire des choix en fonction des données.

Un saut conditionnel n'est effectué qu'à certaines conditions portant sur les flags (par exemple : CF = 1 ou ZF = 0).

Certains mnémoniques de sauts conditionnels sont totalement équivalents, c'est-à-dire qu'ils représentent le même opcode hexadécimal. C'est pour aider le programmeur qu'ils existent parfois sous plusieurs formes.

IV-C-6-a. les sauts de comparaison

  • JE (« Jump if Equal ») fait un saut au label spécifié si et seulement si ZF = 1. Rappelez-vous que ce flag est à 1 si et seulement si le résultat de l'opération précédente vaut zéro. Comme CMP réalise une soustraction, on utilise généralement JE pour savoir si deux nombres sont égaux.

Exemple :

Image non disponible

Mnémonique équivalent : JZ (« Jump if Zero »)

  • JG (« Jump if Greater ») fait un saut au label spécifié si et seulement si ZF = 0 et SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur à un autre.

Exemple:

Image non disponible

Mnémonique équivalent : JNLE (« Jump if Not Less Or Equal »)

  • JGE (« Jump if Greater or Equal ») fait un saut au label spécifié si et seulement si SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNL (« Jump if Not Less »)

  • JL (« Jump if Less ») fait un saut au label spécifié si et seulement si SF <> OF. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNGE (« Jump if Not Greater Or Equal »)

  • JLE (« Jump if Less Or Equal ») fait un saut au label spécifié si et seulement si SF <> OF ou ZF = 1. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNG (« Jump if Not Greater »)

  • JA (« Jump if Above ») fait un saut au label spécifié si et seulement si ZF = 0 et CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur à un autre.

Mnémonique équivalent : JNBE (« Jump if Not Below Or Equal »)

  • JAE (« Jump if Above or Equal ») fait un saut au label spécifié si et seulement si CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNB (« Jump if Not Below »)

  • JB (« Jump if Below ») fait un saut au label spécifié si et seulement si CF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNAE (« Jump if Not Above Or Equal »)

  • JBE (« Jump if Below or Equal ») fait un saut au label spécifié si et seulement si CF = 1 ou ZF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNA (« Jump if Not Above »)

IV-C-6-a-i. les sauts de test sur les flags
Ces instructions testent un flag unique et exécutent ou non le saut selon la valeur de ce flag.
  • JC (« Jump if Carry ») fait un saut au label spécifié si et seulement si CF = 1.

Remarques : Ce mnémonique correspond au même opcode que JB. Il est souvent employé pour vérifier que l'appel d'une interruption n'a pas déclenché d'erreur.

Exemple :

Image non disponible

Si l'appel de la fonction 3Eh de l'interruption 21H se solde par une erreur, alors la CF vaudra 1 et le saut sera accompli. Dans le cas opposé, l'exécution continuera normalement de manière linéaire.

  • JNC (« Jump if not Carry ») fait un saut au label spécifié si et seulement si CF = 0.
  • JZ (« Jump if Zero ») fait un saut au label spécifié si et seulement si ZF = 1. Ce mnémonique correspond au même opcode que JE.
  • JNZ (« Jump if not Zero ») fait un saut au label spécifié si et seulement si ZF = 0. Ce mnémonique correspond au même opcode que JNE.
  • JS (« Jump if Sign ») fait un saut au label spécifié si et seulement si SF = 1.
  • JNS (« Jump if not Sign ») fait un saut au label spécifié si et seulement si SF = 0.
  • JO (« Jump if Overflow ») fait un saut au label spécifié si et seulement si OF = 1.
  • JNO (« Jump if not Overflow ») fait un saut au label spécifié si et seulement si OF = 0.
  • JP (« Jump if Parity ») fait un saut au label spécifié si et seulement si PF = 1.
  • JNP (« Jump if not Parity ») fait un saut au label spécifié si et seulement si PF = 0.
IV-C-6-a-ii. le saut de test sur le registre CX
JCXZ (« Jump if CX = Zero ») fait un saut au label spécifié si et seulement si CX = 0.

IV-C-6-b. Les instructions arithmétiques

IV-C-6-b-i. L'instruction INC (« Increment »)
Syntaxe : INC Destination

Description : Incrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

Exemple : INC CL

IV-C-6-b-ii. L'instruction ADD (« Addition »)
Syntaxe : ADD Destination , Source

Description : Ajoute Source à Destination.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : ADD byte ptr [ VARIABLE + DI], 5

IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)
Syntaxe : ADC Destination , Source

Description : Ajoute ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-iv. L'instruction DEC (« Decrement »)
Syntaxe : DEC Destination

Description : Décrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

IV-C-6-b-v. L'instruction SUB (« Substract »)
Syntaxe : SUB Destination , Source

Description : Soustrait Source à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)
Syntaxe : SBB Destination , Source

Description : Soustrait ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vii. L'instruction MUL (« Multiply »)
Syntaxe : MUL Source

Description : Effectue une multiplication d'entiers non signés .

  • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
  • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
  • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

Indicateurs affectés : CF, OF

Remarque : Source ne peut être une valeur immédiate.

Exemples : MUL CX

MUL byte ptr [ TOTO ]

IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)
Syntaxe : IMUL Source

IMUL Destination , Source

IMUL Destination , Source , Valeur

Description : Effectue une multiplication d'entiers signés .

IMUL Source :

  • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
  • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
  • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

IMUL Destination , Source : Multiplie Destination par Source et place le résultat dans Destination .

IMUL Destination , Source , Valeur : Multiplie Source par Valeur et place le résultat dans Destination .

Indicateurs affectés : CF, OF

IV-C-6-b-ix. L'instruction DIV (« Divide »)
Syntaxe : DIV Source

Description : Effectue une division euclidienne d'entiers non signés .

  • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
  • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
  • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

Indicateurs affectés : AF, OF, PF, SF, ZF

Remarque : Source ne peut être une valeur immédiate.

IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)
Syntaxe : IDIV Source

Description : Effectue une division euclidienne d'entiers signés .

  • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
  • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
  • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-xi. L'instruction NEG (« Negation »)
Syntaxe : NEG Destination

Description : Forme le complément à 2 de Destination , i.e. prend l'opposé de Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : À ne pas confondre avec NOT, qui forme le complément à 1.

IV-C-6-c. Les instructions logiques

IV-C-6-c-i. L'instruction NOT (« Logical NOT »)
Syntaxe : NOT Destination

Description : Effectue un NON logique bit à bit sur Destination (i.e. chaque bit de Destination est inversé).

IV-C-6-c-ii. L'instruction OR (« Logical OR »)
Syntaxe : OR Destination , Source

Description : Effectue un OU logique inclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Afin d'optimiser la taille et les performances du programme, on peut utiliser l'instruction “OR AX, AX” à la place de “CMP AX, 0”.
En effet, un OU bit à bit entre deux nombres identiques ne modifie pas Destination et est exécuté « infiniment » plus rapidement qu'une soustraction. Comme les flags sont affectés, les sauts conditionnels sont possibles.

IV-C-6-c-iii. L'instruction AND (« Logical AND »)
Syntaxe : AND Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)
Syntaxe : TEST Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat n'est pas conservé, donc Destination n'est pas modifié. Seuls les flags sont affectés.

Cet opérateur est souvent utilisé pour tester certains bits de Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Exemple :

IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)
Syntaxe : XOR Destination , Source

Description : Effectue un OU logique exclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Pour remettre un registre à zéro, il est préférable de faire “XOR AX, AX” que “MOV AX, 0”. En effet, le résultat est le même, mais la taille et surtout la vitesse d'exécution de l'instruction sont très largement optimisées.

IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)
Syntaxe : SHL Destination , Source

Description : Décale les bits de Destination de Source positions vers la gauche. Les bits les plus à droite sont remplacés par des zéros.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : SHL AX, 4 ;permet de multiplier par 16 de façon infiniment plus rapide que MUL

Mnémonique équivalent : SAL (« Shift Arithmetical Left »)

IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)
Syntaxe : SHR Destination , Source

Description : Décale les bits de Destination de Source positions vers la droite. Les bits les plus à gauche sont remplacés par des zéros.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Mnémonique équivalent : SAR (« Shift Arithmetical Right »)

IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)
Syntaxe : ROL Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. Le dernier bit à être sorti à gauche et à être rentré à droite est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)
Syntaxe : ROR Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la droite. Le dernier bit à être sorti à droite et à être rentré à gauche est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)
Syntaxe : RCL Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. CF est utilisé comme intermédiaire : chaque bit qui sort à gauche est placé dans CF, et le contenu de CF est ensuite réinséré à droite. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)
Syntaxe : RCR Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la droite. CF est utilisé comme intermédiaire : chaque bit qui sort à droite est placé dans CF, et le contenu de CF est ensuite réinséré à gauche. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-d. Les instructions de manipulation des flags

IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)
Syntaxe : CLC

Description : Met CF à 0.

Indicateurs affectés : CF

IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)
Syntaxe : STC

Description : Met CF à 1.

Indicateurs affectés : CF

IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)
Syntaxe : CLD

Description : Met DF à 0.

Indicateurs affectés : DF

IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)
Syntaxe : STD

Description : Met DF à 1.

Indicateurs affectés : DF

IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)
Syntaxe : CLI

Description : Met IF à 0.

Indicateurs affectés : IF

IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)
Syntaxe : STI

Description : Met IF à 1.

Indicateurs affectés : IF

IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)
Syntaxe : CMC

Description : Inverse CF.

Indicateurs affectés : CF

IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)
Syntaxe : LAHF

Description : Charge dans AH l'octet de poids faible du registre des indicateurs.

IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)
Syntaxe : SAHF

Description : Stocke les bits de AH dans le registre des indicateurs.

IV-C-6-e. Les instructions de gestion de la pile

IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)
Syntaxe : PUSH Source

Description : Empile le mot Source. SP est décrémenté de 2.

Remarques : Source ne peut être une valeur immédiate. Il est possible d'abréger votre code source en écrivant par exemple “PUSH AX BX BP”. Le compilateur écrira alors trois fois l'instruction PUSH du langage machine. Il est possible également d'empiler des doubles-mots.

IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)
Syntaxe : POP Destination

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans Destination. SP est incrémenté de 2.

IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)
Syntaxe : PUSHF

Description : Empile le registre des indicateurs. SP est décrémenté de 2.

Remarque : PUSHFD empile le registre des indicateurs codé sur 32 bits.

IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)
Syntaxe : POPF

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans le registre des indicateurs. SP est incrémenté de 2.

Indicateurs affectés : Tous

Remarque : POPFD est utilisé pour un registre des indicateurs codé sur 32 bits.

IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)
Syntaxe : PUSHA

Description : Empile AX, BX, CX, DX, BP, SI, DI et SP.

Remarque : PUSHAD est utilisé pour des registres de 32 bits.

IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)
Syntaxe : POPA

Description : Restaure AX, BX, CX, DX, BP, SI, DI et SP à partir de la pile.

Remarque : POPAD est utilisé pour des registres de 32 bits.

IV-C-6-f. Les instructions de gestion des chaînes d'octets

IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)
Syntaxe : MOVSB

Description : Copie l'octet adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Pour copier plusieurs octets, faire REP MOVSB (« Repeat Move String Byte »). Le nombre d'octets à copier doit être transmis dans CX de même que pour un LOOP.

Exemple :

Image non disponible
IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)
Syntaxe : SCASB

Description : Compare l'octet adressé par ES:DI avec AL. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarques : Pour comparer plusieurs octets, faire “REP SCASB” ou “REPE SCASB” (« Repeat until Egal »), ou encore “REPZ SCASB” (« Repeat until Zero »). Ces trois préfixes sont équivalents.
Le nombre d'octets à comparer doit être transmis dans CX. La boucle ainsi créée s'arrête si CX = 0 ou si le caractère pointé par ES:DI est le même que celui contenu dans AL (i.e. si ZF = 1). On peut ainsi rechercher un caractère dans une chaîne.
Pour répéter au contraire la comparaison jusqu'à ce que ZF = 0, c'est-à-dire jusqu'à ce que AL et le caractère adressé par ES:DI diffèrent, utiliser REPNE ou REPNZ.

Exemple :

Image non disponible
IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)
Syntaxe : LODSB

Description : Charge dans AL l'octet adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour MOVSB.

Syntaxe : STOSB

Description : Stocke le contenu de AL dans l'octet adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

Syntaxe : CMPSB

Description : Compare l'octet adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g. Les instructions de gestion des chaînes de mots

Ce sont les mêmes que les précédentes, hormis qu'elles traitent des mots et non des octets et que leur nom se termine par un ‘W' au lieu du ‘B'.
IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)
Syntaxe : MOVSW

Description : Copie le mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Pour copier plusieurs mots, faire “REP MOVSW”. Le nombre de mots à copier doit être transmis dans CX.

IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)
Syntaxe : SCASWs

Description : Compare le mot adressé par ES:DI avec AX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)
Syntaxe : LODSW

Description : Charge dans AX le mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)
Syntaxe : STOSW

Description : Stocke le contenu de AX dans le mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)
Syntaxe : CMPSW

Description : Compare le mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots

Elles traitent des doubles-mots et leur nom se termine par un ‘D'.
IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)
Syntaxe : MOVSD

Description : Copie le double-mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Pour copier plusieurs doubles-mots, faire “REP MOVSD”. Le nombre de doubles-mots à copier doit être transmis dans CX.

IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)
Syntaxe : SCASD

Description : Compare le double-mot adressé par ES:DI avec EAX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)
Syntaxe : LODSD

Description : Charge dans EAX le double-mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)
Syntaxe : STOSD

Description : Stocke le contenu de EAX dans le double-mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)
Syntaxe : CMPSD

Description : Compare le double-mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-i. Les instructions d'appel

IV-C-6-i-i. L'instruction CALL (« Procedure Call »)
Syntaxe : CALL MaProc

Description : Appel de procédure. Si MaProc se trouve dans un segment extérieur, le processeur empile CS. Ensuite, dans tous les cas, il empile IP et fait un saut à l'étiquette MaProc.

IV-C-6-i-ii. L'instruction RET (ou RETN)
Syntaxe : RET

Description : Retour de procédure se trouvant à l'intérieur du segment (NEAR). Un mot est dépilé et placé dans IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-i-iii. L'instruction RETF
Syntaxe : RETF

Description : Retour de procédure se trouvant à l'extérieur du segment (FAR). Deux mots sont dépilés et placés dans CS:IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-j. Les instructions de boucle

IV-C-6-j-i. L'instruction LOOP
Syntaxe : LOOP MonLabel

Description : Décrémente CX, puis, si CX <> 0, fait un saut à MonLabel.

IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)
Syntaxe : LOOPE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 1, fait un saut à MonLabel.

Mnémonique équivalent : LOOPZ

IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)
Syntaxe : LOOPNE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 0, fait un saut à MonLabel.

Mnémonique équivalent : LOOPNZ

IV-C-6-k. Les instructions d'adressage

IV-C-6-k-i. L'instruction LEA (« Load effective address »)
Syntaxe : LEA Destination, Source

Description : Charge l'offset de la source dans le registre Destination.

Exemple : LEA BP, word ptr [BP + TOTO]

IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)
Syntaxe : LDS Destination, Source

Description : Transfère dans DS:Destination le contenu de la mémoire adressée par Source.

IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)
Syntaxe : LES Destination, Source

Description : Transfère dans ES:Destination le contenu de la mémoire adressée par Source.

IV-C-6-l. Les instructions de conversion arithmétique

Remarque préliminaire : Nous n'expliciterons pas toutes ces instructions.
IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)
Syntaxe : CBW

Description : Convertit l'octet signé stocké dans AL en un mot (signé) stocké dans AX. Ainsi, si AL est négatif, AH sera rempli de 1 binaires, sinon, AH sera mis à 0.

IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)
Syntaxe : CWD

Description : Convertit le mot signé stocké dans AX en un double-mot (signé) stocké dans DX:AX. Ainsi, si AX est négatif, DX sera rempli de 1 binaires, sinon DX sera mis à 0.

IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)
Syntaxe : MOVSX Destination, Source

Description : Déplace le contenu signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. Si Source est négatif, la partie haute de Destination sera remplie de 1 binaires, sinon elle sera remplie de 0.

IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)
Syntaxe : MOVZX Destination, Source

Description : Déplace le contenu non signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. La partie haute de Destination sera donc mise à 0.

IV-C-6-l-xi. L'instruction XLAT (« Translate »)

IV-C-6-m. Les instructions d'entrée-sortie

IV-C-6-m-i. L'instruction IN (« Input from port »)
Syntaxe : IN Destination, Port

Description : Charge un octet ou un mot depuis un port d'entrée-sortie dans AL ou AX. Port peut être DX ou bien une constante de 8 bits.

IV-C-6-m-ii. L'instruction OUT (« Output to port »)
Syntaxe : OUT Port, Source

Description : Écrit dans Port la valeur contenue dans Source. Source ne peut être que AL ou AX. Port est une constante ou bien DX.


V

V. QUATRIÈME PARTIE

Remarque préliminaire : Cette partie présente quelques-unes des interruptions du DOS qui servent à manier les fichiers d'un disque. Nous la conclurons par l'étude d'un exemple de programme qui crypte un fichier choisi par l'utilisateur.

V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES

Sous DOS, il existe deux méthodes pour accéder aux fichiers et pour chacune d'elles un lot d'interruptions spécifiques. La première est la méthode des FCB (« File contol block »). Elle est rarement utilisée, aussi ne l'aborderons-nous pas. Nous étudierons le principe de la méthode des « handles ».

Pour lire ou écrire des données dans un fichier, il est nécessaire de l'ouvrir, c'est-à-dire de le charger en mémoire. Quand toutes les opérations de lecture et d'écriture auront été effectuées, le fichier devra être refermé afin d'enregistrer les éventuelles dernières modifications et surtout de libérer la mémoire occupée.

V-A-1. Ouverture d'un fichier

On ouvre un fichier en appelant la fonction 3dh de l'interruption 21h. Celle-ci attend comme paramètre dans DS:DX l'adresse d'une chaîne de caractères qui contient le chemin d'accès au fichier sur un disque, par exemple “C:\MonDoss\MonFic.txt”.

Remarque : il n'est pas indispensable de mentionner le chemin d'accès complet : par défaut, le fichier sera cherché à partir du dossier courant.

Remarque importante : La chaîne doit être impérativement suivie de l'octet 00h qui sert à marquer sa fin.

Il nous faut également spécifier le mode d'accès en écrivant dans AL un 0 (si on veut ouvrir le fichier en lecture seule), un 1 (si on veut l'ouvrir en écriture seule) ou un 2 (lecture ET écriture).

Si l'interruption échoue, le flag CF sera mis à 1 sans que le fichier soit ouvert. Dans le cas contraire, CF est mis 0 et le registre AX contient un petit nombre entier (par exemple 5) appelé « handle » (ce qui signifie « poignée ») du fichier. Ce handle représente le fichier. C'est lui qu'il faudra désormais invoquer pour effectuer des opérations de lecture ou d'écriture, et non pas le chemin d'accès.
En effet, les chemins d'accès ne sont donc plus d'aucune utilité puisque les fichiers sont ouverts dans la mémoire vive.

V-A-2. Lecture dans un fichier

Une fois le fichier ouvert, on peut le lire avec la fonction 3fh. Il suffit de mentionner le handle dans BX, le nombre d'octets à lire dans CX, et l'adresse d'un buffer dans DS:DX.
Au cas où vous ne sauriez pas ce qu'est un buffer (ou tampon), sachez que c'est simplement une variable (généralement une chaîne de caractères) destinée à recevoir des données (ou à en fournir). Dans notre cas, le buffer va recevoir les octets lus dans le fichier.

Après l'appel, AX contient le nombre d'octets qui ont été effectivement lus (il peut être inférieur à la taille demandée si le fichier n'est pas assez long). En cas de problème, CF sera mis à 1.

V-A-3. Écriture dans un fichier

Pour écrire des données, on procède de même avec la fonction 40h. Les paramètres sont les mêmes que pour la fonction 3fh. Le buffer contient cette fois les octets à écrire. Après l'appel, le nombre d'octets qui ont été effectivement écrits est stocké dans AX (il sera être inférieur à la taille spécifiée si le disque est plein).

Les données sont écrites sur le disque dur que le tampon (dans la mémoire vive) est plein.

V-A-4. Existence d'un pointeur de fichier

Une question se pose cependant : à quel endroit du fichier les données sont-elles lues (ou écrites) ?

Réponse : quand un fichier est ouvert, un pointeur spécial pointant vers le début du fichier est créé. La première opération de lecture (ou d'écriture) se fera donc au début du fichier.
Mais entre chaque opération, le pointeur est incrémenté de la taille des données que l'on a lues (ou écrites). La deuxième opération se fera donc sur les octets qui suivent ceux de la première.

Remarque : Il est possible de modifier directement le pointeur de fichier : voyez pour cela la fonction 42h…

V-A-5. Fermeture d'un fichier

Pour terminer, le fichier doit être refermé. Les modifications éventuellement apportées et non enregistrées seront écrites sur le disque, et le handle sera libéré. C'est la fonction 3eh qui se charge de tout cela. Elle attend simplement le handle du fichier dans BX. Et comme d'habitude, CF vaut 1 après l'appel si des erreurs ont été rencontrées.

Remarque : Attention lorsque vous laissez le fichier ouvert longtemps afin d'y ajouter progressivement des données ! Si le système plante, vous perdrez les données qui se trouvent dans le tampon à ce moment. C'est pourquoi il est conseillé de forcer régulièrement l'écriture sur le disque en refermant le fichier.

V-A-6. Conclusion

Le tableau suivant récapitule ces différentes étapes :
Fonction | Description | Paramètres
3d h | Ouvrir le fichier | - DS:DX : adresse d'une chaîne contenant le chemin d'accès - AL : mode d'accès
3e h | Fermer le fichier | - BX : handle
3f h | Lire le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer
40 h | Écrire dans le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer

V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS

Pour rechercher un fichier (ou un dossier), on se sert des fonctions 4eh (« Find First ») et 4fh (« Find Next »).

Imaginons par exemple que nous voulions chercher dans le dossier courant tous les fichiers qui portent l'extension “ .com ” afin de les supprimer. Comment devons-nous nous y prendre ?

V-B-1. La fonction 4eh

La fonction 4eh sert à définir des critères de recherche et à trouver le premier fichier qui correspond à ces critères (s'il existe).

On doit passer dans DS:DX l'adresse de la chaîne de caractères qui contient le masque de recherche (dans notre exemple, ce masque est “ *.com ”). Par défaut, les fichiers sont cherchés dans le dossier courant. Mais on peut évidemment spécifier un autre chemin dans le masque.

Remarque importante : afin que le DOS puisse connaître sa taille, le masque doit impérativement être terminé par l'octet 00h !

On écrit également dans CX les attributs des fichiers que l'on désire trouver. Si CX vaut 0, seuls les fichiers « normaux » pourront être trouvés. En fait, chaque bit de CL représente un attribut, comme le montre le tableau ci-dessous :

Bit | Signification
1 | Lecture seule
2 | Fichier caché
3 | Fichier système
4 | Volume
5 | Répertoire
6 | Fichier
7 | (Aucune…)
8 | (Aucune…)

Pour demander à la fonction 4eh de ne pas oublier les fichiers cachés, il suffit donc de charger CX avec la valeur 2 (bit numéro 2 = 1). De même, l'attribut 00000111 b (soit 7) nous permettra de trouver les fichiers en lecture seule, les fichiers cachés et les fichiers systèmes.

Remarque : Ne vous souciez pas trop des bits numéro 4 et 6. Laissez-les à 0.

Une fois que les paramètres ont été ajustés, on peut appeler la fonction 4eh. Si aucun fichier n'a été trouvé, le flag CF est mis à 1 . On doit donc faire un test sur CF pour savoir si la recherche peut continuer ou si elle doit s'arrêter.
Si au contraire la fonction a trouvé un fichier, les caractéristiques de ce fichier (i.e. son nom, sa taille, ses attributs…) sont inscrites dans une zone de la mémoire appelée DTA (« Disk Transfer Area »).

Mais où se trouve donc cette DTA et à quoi ressemble-t-elle ?

Réponse : par défaut, le DOS place la DTA dans le PSP de votre programme, à l'offset 80h.

Remarque : il vous est naturellement possible de la déplacer en faisant appel à la fonction 1ah.

Voici la structure de la DTA :

Adresse | Description | Taille (octets)
00 h | Lettre du lecteur (0=courant, 1=A, 2=B…) sur lequel se trouve le fichier | 1
01 h | Modèle de la recherche | 11
0C h | Réservé | 9
15 h | Attributs du fichier | 1
16 h | Heure du fichier | 2
18 h | Date du fichier | 2
1A h | Taille du fichier | 4
1E h | Nom du fichier avec l'extension | 13

Puisque par défaut la DTA est située dans le PSP à l'offset 80h, le nom du fichier trouvé est écrit à l'offset 80h + 1eh = 9eh. De même, la taille se trouve à l'offset 9ah, etc.

V-B-2. La fonction 4fh

Jusqu'à présent, nous n'avons trouvé qu'un seul fichier ! Pour poursuivre la recherche, appelez la fonction 4fh sans écrire aucun paramètre dans les registres. Les caractéristiques de votre recherche ont été mémorisées dans la DTA : vous n'avez donc pas à les rappeler. De même que pour la fonction 4eh, CF est mis à 1 si aucun nouveau fichier n'a été trouvé. C'est le signe que vous pouvez arrêter votre recherche.

V-B-3. Conclusion

Résumons-nous :
Fonction | Description | Paramètres
4e h | Trouver le premier fichier qui correspond aux caractéristiques spécifiées | - DS:DX : adresse d'une chaîne contenant le masque - CX : attributs
4f h | Trouver le prochain fichier | Aucun !

À titre d'exemple, écrivons à présent le programme que nous évoquions tout à l'heure : “ trouver tous les fichiers COM du dossier courant et les effacer ”.

V-B-4. EXEMPLE DE PROGRAMME

V-B-5. Entrée bufferisée au clavier

L'exemple que nous proposons ici fait appel à une saisie au clavier. Comme nous n'en avons pas encore rencontré, ce paragraphe explique brièvement comment utiliser les fonctions 0ah et 0ch.

On peut se servir de la fonction 0ah pour lire une chaîne de caractères au clavier. Le seul paramètre à fournir est l'adresse d'un buffer. Comme il est souvent indispensable d'effacer le buffer avant la lecture, il est préférable de ne pas utiliser la fonction 0ah directement, mais d'appeler la fonction 0ch. En effet, celle-ci commence par effacer le buffer, puis appelle la fonction 0ah (ou une fonction voisine dont le numéro doit être transmis dans AL).

Voici le code à écrire :

Image non disponible

La seule difficulté est dans la déclaration du buffer. Voici comment vous devez vous y prendre si vous considérez que l'utilisateur pourra entrer au plus n caractères :

Image non disponible

Le premier octet de “maChaine” doit contenir le nombre d'octets maximal qui pourront être entrés, plus 1. Pourquoi plus 1 ? Tout simplement parce que le DOS met à la fin des caractères tapés le code ASCII 13 (retour chariot).

Pour comprendre la suite de la déclaration, il faut savoir de quelle manière le DOS transmet les caractères qui ont été lus. Après la lecture, le deuxième octet du buffer contiendra le nombre exact d'octets lus (sans compter le 13 final). La chaîne proprement dite ne commencera donc qu'au troisième octet. Puisqu'un octet est utilisé pour donner au DOS le nombre maximal de caractères autorisés, un autre pour recevoir le nombre de caractères lus effectivement, et encore un autre pour recevoir le code ASCII 13, le buffer doit comporter trois octets de plus que la taille maximale de la chaîne. C'est pourquoi on écrit :

maChaine db n+1, ?, n dup(?), ?

V-B-6. Le programme

Le programme suivant demande à l'utilisateur d'entrer le nom d'un fichier se trouvant dans le dossier courant puis crypte ce fichier en appliquant un NOT logique sur chaque octet. Naturellement, il n'est pas optimisé du tout. Ce n'est qu'un exemple !

Image non disponible

Suggestion : quand vous aurez compris ce petit programme, essayez donc de l'améliorer ! Par exemple, il faudrait que l'utilisateur puisse taper le nom du fichier à crypter directement comme paramètre du programme (i.e. “CRY MONFICH.EXE”). Voyez pour cela la structure du PSP telle que présentée dans la première partie. De plus, le programme devrait pouvoir trouver automatiquement le nom du fichier d'arrivée : ce serait le même que celui du fichier à crypter, mais avec une extension différente (par exemple “.cry”).
Vous pourriez également étendre les possibilités du logiciel en permettant à l'utilisateur de saisir non plus un simple nom de fichier, mais un masque complexe avec des chemins d'accès et des caractères “jokers” comme '*' et '?'. Vous aurez donc besoin des fonctions de recherche 4eh et 4fh. L'utilisateur doit de surcroît pouvoir choisir s'il veut crypter un fichier ou bien le décrypter…


VI

VI. CONCLUSION

Voilà. Nous espérons que vous avez compris l'essentiel de ce cours, à savoir la logique de la programmation en assembleur. Les connaissances que vous avez acquises en lisant la fin du cours devraient vous permettre de mieux saisir les premiers chapitres. C'est pourquoi nous vous invitons à relire ce tutoriel.

Certaines notions doivent encore vous paraître obscures. Maintenant que vous vous êtes familiarisé avec l'assembleur, commencez par écrire et tester quelques petits programmes COM inutiles et très simples. Vous verrez qu'au bout de quelques essais, tout ce que vous avez lu vous semblera très concret.

Lorsque vous aurez écrit vous-même quelques tout petits programmes qui fonctionnent, l'assimilation du langage deviendra alors très rapide, car l'assembleur est un langage très logique et très cohérent.

Nous vous conseillons d'écrire une bibliothèque de petites macros ou procédures qui vous serviront dans tous vos programmes. Par exemple, vous pouvez faire une macro qui affiche un entier à l'écran, ou bien qui convertit une chaîne de caractères en un nombre entier.

Voici un exemple de macro qui renvoie dans AL le nombre de chiffres d'un entier non signé de deux octets passé dans AX :

Image non disponible

N.B. Pensez avant toute chose à vous munir d'une liste des interruptions du DOS. Vous ne pourrez rien faire si vous n'en avez pas !

Bon courage et bonne chance !

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Benoît-M. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique Assembleur : Alcatîz -
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


Les bases de l'asm

Introduction

Quoi de plus ardu que d'essayer de résumer l'asm pour n'en garder que l'essentiel. De plus, comment voulez-vous posséder un bon niveau de programmeur mais aussi d'analyste si des instructions vous font défaut? D'un autre côté, rien ne sert de se leurrer, peu sont ceux qui peuvent se vanter de connaître toutes les instructions sur le bout des doigts ainsi que toutes les astuces qui en découlent. C'est pourquoi je ne perdrais pas ici mon temps à détailler toutes les instructions ni à faire du plagia (en cherchant un peu, Google regorge de tutos plus ou moins bien expliqués et complets sur l'asm). Je vais simplement présenter brièvement l'asm et faire un bref rappel de quelques instructions qui me semblent intéressantes pour le cracking mais sans rentrer dans les détails techniques, ainsi que de quelques astuces utilisées avec celles-ci. Libre à vous de continuer votre apprentissage sur d'autres sites.

Les registres

Les registres principaux sont : EAX, EBX,ECX, EDX, EDI, ESI, EBP, ESP, EIP et les Flags.

Ce sont des petites zones de stockage logées dans le processeur auxquels il a un accès très rapide (beaucoup plus rapide qu'en mémoire).

Ils ont tous une taille de 32 bits (4 bytes).

EAX, EBX, ECX et EDX sont les registres de travail. La partie basse (les 16bits de poids fort) de chacun d'eux peut être utilisée directement mais reste une partie du registre de 32bits, c'est à dire que s'il l'on modifie AX (la partie basse de EAX), on modifiera EAX. En plus de cela, la partie basse peut être de nouveau séparée en 2 parties de 8bits (la partie haute AH et la partie basse AL pour AX, BH et BL pour BX, ...). De nouveau si l'on modifie l'une des 2 parties, c'est tout le registre qui est modifié (AX est modifié donc EAX aussi).

EDI et ESI servent principalement pour des instructions d'adressage (lors de traitements de chaînes de caractères par exemple) mais peuvent aussi servir de registres de travail.

EBP et ESP sont des registres de pile, ESP désigne toujours le sommet de la pile. EBP permet de travailler dans la pile sans devoir utiliser ESP qu'il ne vaut mieux pas modifier.

EIP désigne l'adresse de l'instruction courante.

Le registre de Flags est un registre dont chaque bit (indicateur) indique un état. Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs.

La pile

La pile est un espace mémoire régit par le système LIFO (Last In First Out). La dernière valeur à avoir été empilée sera la première à être dépilée. Imaginez une pile d'assiettes : la dernière que vous avez déposée sur le paquet sera celle que vous reprendrez en première, c'est le principe de la pile. On y accède par des PUSH et des POP

Quelques instructions courantes

NOP (90h)

La plus simple à expliquer : cette instruction ne fait rien! Vous le savez peut-être mais nous ne pouvons modifier la taille d'un programme sans des conséquences désastreuses. C'est là que cette instruction prend toute son utilité. Elle permet de supprimer une instructions sans modifier la taille du programme (si l'instruction à "effacer" tient sur plusieurs bytes remplacez autant de bytes que nécessaires par NOP).

MOV destination, source

Cette instruction copie la valeur de source dans destination. Les 2 opérandes doivent avoir la même taille. Il n'y peut y avoir au maximum qu'un accès mémoire par instruction. Si vous voulez par exemple mettre la valeur de [00401234] dans [00405678], vous devrez faire par exemple:
MOV EBX , [401234]

MOV [405678] , EBX

mov eax , 45 ; Mettre 45 dans eax

ADD destination, source

Cette instruction additionne le contenu de destination à celui de source. Le résultat est placé dans destination.

mov eax,45

add eax,10 ; eax <= eax (45) + 10 donc eax vaudra 55

SUB destination, source

Cette instruction soustrait le contenu de source à celui de destination. Le résultat est placé dans destination.

mov eax,45

sub eax,5 ; eax <= eax (45) - 5 donc eax vaudra 40

INC destination

Cette instruction incrémente destination d'une unité

mov eax,10

inc eax ; eax <= eax + 1

DEC destination

Cette instruction décrémente destination d'une unité

mov eax,10

dec eax ; eax <= eax - 1

JMP label

Modifie l'EIP en l'adresse désignée par label. Ce que l'on désigne par un saut.

jmp label

; instructions non exécutées

label:

; instructions à éxécuter

PUSH source

Place la valeur de source au sommet de la pile.

POP destination

Place la valeur au sommet de la pile dans destination.

mov eax,15

push eax ; Place la valeur de eax (15) sur la pile

pop ebx ; Récupère la valeur au sommet de la pile (15, c'est celle que l'on vient de mettre) et la place dans ebx

CMP destination, source

Cette instruction effectue la soustraction de destination et source mais ne conserve pas le résultat. Seuls les Flags sont modifiés.

Le ZF (Zero Flag) est positionné à 1 si le résultat de la soustraction est nul (donc que les 2 valeurs sont égales).

Le CF (Carry Flag) est positionné à 1 si le résultat ne tient pas sur 32bits (donc que le résultat est est inférieur à 0).

Le SF (Sign Flag) est positionné à 1 si le résultat est négatif.

Le OF (Overflow Flag) est positionné à 1 si le résultat a provoqué un débordement (lorsqu'on travaille avec des nombres signés).

mov eax,5

cmp eax,10 ; eax < à 10 => ZF=0, SF=1 et CF=1

cmp eax,2 ; eax > à 2 => ZF=0, SF=0 et CF=0

cmp eax,5 ; eax = 5 => ZF=1, SF=0 et CF=0

Les sauts conditionnels

Fonctionne comme un JMP mais saute en fonction de l'état de certains bits du registre de Flags.

Les différents sauts sont :

Mnémo | Saut si … | Indicat. testés
JA | Supérieur | CF = 0 et ZF = 0
JAE | Supérieur ou égal | CF = 0
JB | Inférieur | CF = 1
JBE | Inférieur ou égal | CF = 1 ou ZF = 1
JC | CF = 1 | CF = 1
JCXZ | Registre CX à 0 | CX = 0
JE | Egal | ZF = 1
JG* | Supérieur | ZF = 0 et SF = OF
JGE* | Supérieur ou égal | SF = OF
JL* | Inférieur | SF<>OF
JLE* | Inférieur ou égal | ZF = 1 ou SF<>OF
JNA | Pas supérieur | CF = 1 ou ZF = 1
JNAE | Pas supérieur ni égal | CF = 1
JNB | Pas inférieur | CF = 0
JNBE | Pas inférieur ni égal | CF = 0 et ZF = 0
JNC | CF = 0 | CF = 0
JNE | Pas égal | ZF = 0
JNG* | Pas supérieur | ZF = 1 et SF<>OF
JNGE* | Pas supérieur ni égal | SF<>OF
JNL* | Pas inférieur | SF = OF
JNLE* | Pas inférieur ni égal | ZF = 0 et SF = OF
JNO | OF = 0 | OF = 0
JNP | PF=0 | PF = 0
JNS* | SF=0 | SF = 0
JNZ | ZF=0 | ZF = 0
JO* | OF=1 | OF = 1
JP | PF=1 | PF = 1
JPE | Parité paire | PF = 1
JPO | Parité impaire | PF = 0

* Utiliser avec des nombres signés

mov eax,2

cmp eax,5

je lblfin ; On ne saute pas puisque 2<>5

; Instructions qui vont être exécutées puisque eax <> 5 et donc on ne sautera pas

lblfin:

Les opérations logiques

Les opérations logiques travaillent toutes bit à bit. Elle peuvent servir à comparer deux valeurs, tester certains bits mais aussi à forcer certains bits à une certaine valeur.

AND destination, source

Le AND effectue un ET LOGIQUE de destination et source. Le AND peut se traduire par une multiplication de bits.

Voici la table logique :

A | B | AND
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

mov al,14h ; 00010100b

mov bl,2Fh ; 00101111b

and al,bl ; 00000100b = 04h => al va prendre la valeur 04h

OR destination, source

Le OR effectue un OU inclusif de destination et source. Le OR peut se traduire par une addition de bits sans report.

A | B | OR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1

mov al,14h ; 00010100b

mov bl,2Fh ; 00101111b

or al,bl ; 00111111b = 3Fh => al va prendre la valeur 3Fh

XOR destination, source

Le XOR effectue un OU inclusif de destination et source. Le XOR peut se traduire par une addition de bits avec report.

A | B | XOR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

mov al,14h ; 00010100b

mov bl,2Fh ; 00101111b

xor al,bl ; 00111011b = 3Bh => al va prendre la valeur 3Bh

Voilà, ce n'est évidemment pas complet mais j'espère que cela vous permettra de commencer mes cours sans trop galérer.

Si vous voulez compléter votre apprentissage, et je vous conseille de le faire quitte à garder les liens et à les utiliser par après, voici quelques sites non exhaustifs qui vous permettrons d'apprendre les bases (plus ou moins par ordre de difficulté) :

http://benoit-m.developpez.com/assembleur/tutoriel | Les bases de l'asm
http://asm.developpez.com/intro/index.php | Aussi
http://www.developpez.com/ | Un site regroupant les bases de beaucoup de langages
http://www.deamoncrack.host.sk/Pages/cours-asm.htm | Si vous n'avez pas trop de temps et que vous avez juste besoin d'un rappel
http://www.chez.com/pageasm/ | Intro à la programmation win32 mais aussi DOS
http://win32assembly.online.fr/ | Le site de référence pour la programmation win32 mais en anglais
les opcodes | Très complet mais en anglais

Bon crack,

Crisanar

Dernière mise à jour le 26/03/2016 13:54


Trigonométrie

Apprendre l'assembleur

Table des matières Plier Déplier

I. Introduction
  • II. PREMIÈRE PARTIE
      • II-A. L'ARITHMÉTIQUE SIGNÉE
      • II-B. LA MÉMOIRE VIVE
        • II-B-1. La segmentation de la mémoire
        • II-B-2. Structure d'un programme en mémoire
        • II-B-3. Les fichiers COM
        • II-B-4. Les fichiers EXE
        • II-B-5. Les cycles de lecture-écriture
      • II-C. LE MICROPROCESSEUR - LES REGISTRES
        • II-C-1. Généralités sur les registres
        • II-C-2. Les registres généraux
        • II-C-3. Les registres de segment
        • II-C-4. Les registres d'offset
        • II-C-5. Le registre des indicateurs
      • II-D. LES INTERRUPTIONS
        • II-D-1. Introduction
        • II-D-2. La table des vecteurs d'interruptions
        • II-D-3. Sauvegarde de l'état des registres lors de l'appel
    • III. DEUXIÈME PARTIE
      • III-A. PREMIER EXEMPLE : LES FICHIERS COM
      • III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE
    • IV. TROISIÈME PARTIE
      • IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE
        • IV-A-1. Les définitions de données
        • IV-A-2. L'adressage
          • IV-A-2-a. L'adressage immédiat
          • IV-A-2-b. L'adressage indexé et/ou basé
      • IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS
        • IV-B-1. Les sauts inconditionnels
          • IV-B-1-a. Les procédures
            • IV-B-1-a-i. appels de procédures
            • IV-B-1-a-ii. le passage des paramètres
          • IV-B-1-b. Les macros
          • IV-B-1-c. La directive EQU
          • IV-B-1-d. L'inclusion de fichiers
      • IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE
        • IV-C-1. L'instruction NOP (« No Operation »)
        • IV-C-2. L'instruction MOV (« Move »)
        • IV-C-3. L'instruction XCHG (« Exchange »)
        • IV-C-4. L'instruction JMP (« Jump »)
        • IV-C-5. L'opérateur CMP (« Compare »)
        • IV-C-6. Les instructions de saut conditionnel
          • IV-C-6-a. les sauts de comparaison
            • IV-C-6-a-i. les sauts de test sur les flags
            • IV-C-6-a-ii. le saut de test sur le registre CX
          • IV-C-6-b. Les instructions arithmétiques
            • IV-C-6-b-i. L'instruction INC (« Increment »)
            • IV-C-6-b-ii. L'instruction ADD (« Addition »)
            • IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)
            • IV-C-6-b-iv. L'instruction DEC (« Decrement »)
            • IV-C-6-b-v. L'instruction SUB (« Substract »)
            • IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)
            • IV-C-6-b-vii. L'instruction MUL (« Multiply »)
            • IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)
            • IV-C-6-b-ix. L'instruction DIV (« Divide »)
            • IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)
            • IV-C-6-b-xi. L'instruction NEG (« Negation »)
          • IV-C-6-c. Les instructions logiques
            • IV-C-6-c-i. L'instruction NOT (« Logical NOT »)
            • IV-C-6-c-ii. L'instruction OR (« Logical OR »)
            • IV-C-6-c-iii. L'instruction AND (« Logical AND »)
            • IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)
            • IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)
            • IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)
            • IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)
            • IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)
            • IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)
            • IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)
            • IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)
          • IV-C-6-d. Les instructions de manipulation des flags
            • IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)
            • IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)
            • IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)
            • IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)
            • IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)
            • IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)
            • IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)
            • IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)
            • IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)
          • IV-C-6-e. Les instructions de gestion de la pile
            • IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)
            • IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)
            • IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)
            • IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)
            • IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)
            • IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)
          • IV-C-6-f. Les instructions de gestion des chaînes d'octets
            • IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)
            • IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)
            • IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)
          • IV-C-6-g. Les instructions de gestion des chaînes de mots
            • IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)
            • IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)
            • IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)
            • IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)
            • IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)
          • IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots
            • IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)
            • IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)
            • IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)
            • IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)
            • IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)
          • IV-C-6-i. Les instructions d'appel
            • IV-C-6-i-i. L'instruction CALL (« Procedure Call »)
            • IV-C-6-i-ii. L'instruction RET (ou RETN)
            • IV-C-6-i-iii. L'instruction RETF
          • IV-C-6-j. Les instructions de boucle
            • IV-C-6-j-i. L'instruction LOOP
            • IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)
            • IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)
          • IV-C-6-k. Les instructions d'adressage
            • IV-C-6-k-i. L'instruction LEA (« Load effective address »)
            • IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)
            • IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)
          • IV-C-6-l. Les instructions de conversion arithmétique
            • IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
            • IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
            • IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
            • IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
            • IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)
            • IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)
            • IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
            • IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
            • IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)
            • IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)
            • IV-C-6-l-xi. L'instruction XLAT (« Translate »)
          • IV-C-6-m. Les instructions d'entrée-sortie
            • IV-C-6-m-i. L'instruction IN (« Input from port »)
            • IV-C-6-m-ii. L'instruction OUT (« Output to port »)
    • V. QUATRIÈME PARTIE
      • V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES
        • V-A-1. Ouverture d'un fichier
        • V-A-2. Lecture dans un fichier
        • V-A-3. Écriture dans un fichier
        • V-A-4. Existence d'un pointeur de fichier
        • V-A-5. Fermeture d'un fichier
        • V-A-6. Conclusion
      • V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS
        • V-B-1. La fonction 4eh
        • V-B-2. La fonction 4fh
        • V-B-3. Conclusion
        • V-B-4. EXEMPLE DE PROGRAMME
        • V-B-5. Entrée bufferisée au clavier
        • V-B-6. Le programme
    • VI. CONCLUSION

    L'auteur

    Benoît-M
    L'article
    Publié le 3 janvier 2003 - Mis à jour le 15 février 2013

    I. Introduction

    « Désormais, pour apprendre le français, il faudra SAVOIR le français ! » disait Coluche. De même, ce tutoriel s'adresse à tous ceux qui connaissent déjà les rudiments d'un langage évolué tel que le BASIC, le FORTRAN, le C ou le PASCAL et qui souhaitent apprendre l'assembleur. Une connaissance même sommaire d'un de ces langages suffit ! Aucune connaissance en programmation système n'est requise : le but de ce cours est justement d'en introduire les fondements.

    Contrairement aux langages évolués, l'assembleur, ou « langage d'assemblage » est constitué d'instructions directement compréhensibles par le microprocesseur : c'est ce qu'on appelle un langage de bas niveau. Il est donc intimement lié au fonctionnement de la machine. C'est pourquoi il est relativement difficile à assimiler, en tout cas beaucoup plus que les langages de haut niveau.

    Cela explique également pourquoi il existe au moins autant de langages d'assemblage que de modèles de microprocesseurs.

    Avant d'apprendre l'assembleur INTEL 80x86, il est donc primordial de s'intéresser à quelques notions de base concernant par exemple la mémoire ou le microprocesseur. C'est là en effet que se trouvent les principales difficultés pour le débutant. Ne soyez pas rebuté par l'abstraction des concepts présentés dans les premiers paragraphes : il est normal que durant la lecture, beaucoup de choses ne soient pas claires dans votre esprit. Tout vous semblera beaucoup plus simple quand nous passerons à la pratique dans le langage assembleur.

    Le microprocesseur peut fonctionner sous deux modes : le mode réel et le mode protégé. Le mode protégé permet d'accéder à 232 octets de mémoire vive, alors que le mode réel ne peut en adresser que 220 = 1 Mo. Nous ne traiterons dans ce cours que le mode réel. C'est celui qu'utilisent la plupart des programmes DOS.

    Convention : toutes les adresses sont écrites en notation hexadécimale. Les autres nombres seront la plupart du temps représentés en base décimale. Dans le cas contraire, nous ajouterons la lettre ‘h' après les chiffres.

    II. PREMIÈRE PARTIE

    II-A. L'ARITHMÉTIQUE SIGNÉE

    On appelle arithmétique non signée l'arithmétique dans laquelle tous les entiers sont positifs. En arithmétique signée au contraire, les nombres peuvent être soit positifs, soit négatifs. Un nombre signé n'est donc pas forcément négatif.

    Les données informatiques se présentent sous la forme d'une succession de chiffres binaires, les bits. Nous supposerons que les systèmes de numération binaire et hexadécimal vous sont déjà familiers.

    Il est en revanche fort possible que vous ne connaissiez pas la façon dont un ordinateur représente les nombres négatifs. Il existe deux conventions : la notation en signe et valeur absolue et la notation en complément à 2.

    La première est extrêmement simple : le premier bit représente le signe, et les autres bits la valeur absolue du nombre. Le bit de signe vaut 1 si le nombre est strictement négatif, 0 sinon.

    Par exemple, le nombre (-14) codé sur 8 bits s'écrit ainsi :

    10001110

    Cette convention n'est quasiment jamais utilisée en informatique. On lui préfère la représentation en complément à 2 dont le principe est le suivant :

    • les nombres positifs sont codés de la même façon qu'en convention « signe et valeur absolue » ;
    • les nombres négatifs sont obtenus en inversant tous les bits, puis en ajoutant 1.

    Exemple : le nombre 14 codé sur 8 bits est représenté ainsi :

    00001110

    et (-14) ainsi :

    • inversion des bits : 11110001 ;
    • ajout d'une unité : 11110010 ;
    • résultat : 11110010.

    Remarque : le résultat intermédiaire, 11110001, est appelé « complément à 1 ».

    Vous allez immédiatement comprendre l'avantage de cette représentation. Faisons la somme de 14 et de (-14), de la même façon que s'il s'agissait d'entiers positifs :

    00001110 + 11110010 = 100000000

    Le résultat étant codé sur 8 bits, le 1 situé à gauche n'est pas pris en compte. On obtient donc 14 + (-14) = 0.

    L'intérêt évident est que la différence de deux nombres peut se calculer avec le même algorithme que leur somme. Il suffit de transformer au préalable le nombre retranché en son opposé. Cette conversion est très simple et très rapide.

    Au contraire, en représentation « signe et valeur absolue », on aurait eu besoin de nombreux algorithmes, car plusieurs cas se présentent.

    Remarque : la représentation en complément à 2 revient en fait à écrire (-1) comme ceci :

    11111111

    (-2) comme cela :

    11111110

    (-3) comme cela :

    11111101

    etc.

    Il y a ainsi une symétrie entre les nombres positifs et les nombres négatifs. Il en résulte que le bit le plus à gauche représente le signe, de la même façon qu'en notation « signe et valeur absolue ». Avant tout calcul, nous pouvons donc affirmer que le nombre 10010101 est négatif.

    II-B. LA MÉMOIRE VIVE

    II-B-1. La segmentation de la mémoire

    Votre PC est conçu pour gérer 1 Mo (soit 220 octets) de mémoire vive en mode réel. Il faut donc 20 bits au minimum pour adresser toute la mémoire. Or en mode réel les bus d'adresses n'ont que 16 bits. Ils permettent donc d'adresser 216 = 65536 octets = 64 Ko, ce qui est insuffisant !

    Afin de pallier ce manque, on utilise deux nombres pour adresser un octet quelconque de la RAM. Le premier est appelé adresse de segment, le second adresse d'offset. Ils seront stockés séparément.

    La mémoire est découpée en segments de 64 Ko chacun. Un segment est donc en quelque sorte un gros bloc de mémoire auquel on peut accéder grâce à une adresse de segment qui désigne son numéro. Par exemple, le premier segment est le segment 0000 (en hexa), le deuxième est le 0001, le quarante-deuxième est le 0029, etc. Chaque numéro est codé sur 16 bits, c'est-à-dire 4 chiffres hexa.

    Pour accéder à un octet particulier dans un segment, il suffit de compter le décalage de cet octet par rapport au début du segment. Ce décalage est obligatoirement inférieur ou égal à 65535 : il tient bien sur 16 bits lui aussi. On appelle ce décalage « offset ».

    L'adresse d'un octet se note XXXX:YYYY où XXXX est l'adresse de segment et YYYY est l'offset (tous deux en notation hexadécimale, bien sûr).

    Par exemple, le dix-septième octet de la RAM (le numéro 16) est situé à l'adresse 0000:0010. De même, l'octet 0000:0100 est l'octet numéro 256. Nous en arrivons à la petite subtilité qu'il convient de bien saisir, sous peine de ne rien comprendre à certains programmes en assembleur.

    On pourrait penser que l'octet qui se trouve à l'adresse 0001:0000 est le numéro 65536. Il n'en est rien. C'est l'octet numéro 16.

    Eh oui ! Les segments ne sont pas situés gentiment les uns à la suite des autres. Ils sont fort mal élevés et n'attendent pas que les segments qui les précèdent soient terminés avant de commencer ! Ils se marchent donc sur les pieds.

    Autrement dit, le deuxième segment ne démarre pas à l'octet 65536 comme il devrait le faire s'il était bien sage, mais à l'octet 16 ! Le troisième démarre à l'octet 32 et ainsi de suite…

    La notion de segment n'est pas tant physique que mathématique : elle sert à se repérer dans la RAM.

    La conséquence immédiate de tout cela est qu'un octet n'a pas une adresse unique. Par exemple, l'octet numéro 66 peut être adressé par 0000:0042, mais aussi par 0001:0032, par 0002:0022, par 0003:0012 ou encore par 0004:0002. Toutes ces adresses sont équivalentes.

    Voilà pour la subtilité. Si vous avez compris, vous devriez être capable de trouver facilement comment on calcule l'adresse effective d'un octet, c'est-à-dire sa position absolue dans la RAM.

    Allez, un petit effort !

    Voici la solution : si l'adresse de l'octet est A17C:022E, alors son adresse effective est A17C x 16 + 022E, soit A17C0 + 022E = A19EE. On a multiplié par 16, car le segment A17C débute à l'octet A17C x 16, puis on a simplement ajouté le décalage.

    Au final, on a bien une adresse sur 20 bits puisqu'on obtient 5 chiffres hexa. Chaque petit bloc de 16 octets s'appelle un paragraphe.

    Image non disponible

    Il est souvent plus simple de considérer qu'un segment est un bloc de taille quelconque qui débute à une adresse effective multiple de 16 et qui permet, à l'aide de son adresse de segment et d'un offset, d'adresser le bloc entier (64 Ko au maximum). Cette définition est à notre avis celle qui a le plus de sens, et nous l'utiliserons tout au long de ce cours.

    II-B-2. Structure d'un programme en mémoire

    Lorsque l'utilisateur exécute un programme, celui-ci est d'abord chargé en mémoire par le système. Le DOS distingue deux modèles de programmes exécutables : les fichiers COM et les fichiers EXE.

    La différence fondamentale est que les programmes COM ne peuvent pas utiliser plus d'un segment dans la mémoire. Leur taille est ainsi limitée à 64 Ko. Les programmes EXE ne sont quant à eux limités que par la mémoire disponible dans l'ordinateur.

    II-B-3. Les fichiers COM

    Lorsqu'il charge un fichier COM, le DOS lui alloue toute la mémoire disponible. Si celle-ci est insuffisante, il le signale à l'utilisateur par un message et annule toute la procédure d'exécution. Dans le cas contraire, il crée le PSP du programme au début du bloc de mémoire réservé, et copie le programme à charger à la suite.

    Mais qu'est-ce donc qu'un PSP ?

    Pour simplifier, le PSP (« Program Segment Prefix ») est une zone de 256 (= 100h) octets qui contient des informations diverses au sujet du programme. C'est dans le PSP que se trouve la ligne de commande tapée par l'utilisateur. Par exemple, le PSP d'un programme appelé MONPROG, exécuté avec la commande « MONPROG monfic.txt /S /H », contiendra la chaîne de caractères suivante : « monfic.txt /S /H ». Le programmeur a ainsi la possibilité d'accéder aux paramètres.

    Voici à titre indicatif la structure simplifiée du PSP (ne vous souciez pas de ce que vous ne comprenez pas : pour l'instant, seules les deux dernières lignes nous intéressent vraiment) :

    Offset | Description | Taille (octets)
00 h | Appel de l'int 20h | 2
02 h | Adresse du 1er segment qui se trouve au-delà du prog. | 2
04 h | Réservé | 1
05 h | Far call de l'int 21h (inutilisé) | 5
0A h | Vecteur de l'int 22h | 4
0E h | Vecteur de l'int 23h | 4
12 h | Vecteur de l'int 24h | 4
16 h | Réservé | 22
2C h | Segment du bloc d'environnement | 2
2E h | Réservé | 82
80 h | Nombre de caractères dans la ligne de commande sans compter le code ASCII 13 (retour chariot) | 1
81 h | Ligne de commande (à partir du caractère espace qui suit le nom du programme) + code ASCII 13 | 127

    À présent que nous connaissons l'existence du PSP, il nous faut revenir sur un point important. Comme nous l'avons dit, un programme COM ne peut comporter qu'un seul segment, bien que le DOS lui réserve la totalité de la mémoire disponible. Ceci a deux conséquences. La première est que les adresses de segment sont inutiles dans le programme : les offsets seuls permettent d'adresser n'importe quel octet du segment. La seconde est que le PSP fait partie de ce segment, ce qui limite à 64 Ko - 256 octets la taille maximale d'un fichier COM. Cela implique également que le programme lui-même débute à l'offset 100h et non à l'offset 0h.

    II-B-4. Les fichiers EXE

    « Pour un fichier EXE, tout est toujours un peu plus compliqué. »
    (Devise du programmeur débutant en assembleur)

    Bien qu'il soit possible de n'utiliser qu'un seul segment à tout faire, la plupart des programmes EXE ont un segment réservé au code (c'est ainsi qu'on appelle les instructions du langage machine), un ou deux autres aux données, et un dernier à la pile.

    La pile est une mémoire très spéciale qui sert comme son nom l'indique à empiler des données de 16 bits de façon temporaire. On peut ensuite retrouver ces données en les dépilant. Le « dépilage » se fait toujours dans l'ordre inverse de l'empilage.

    Le PSP a lui aussi son propre segment. Le programme commence donc à l'offset 0h du segment de code et non à l'offset 100h.

    Afin que le programme puisse être chargé et exécuté correctement, il faut que le système sache où commence et où s'arrête chacun de ces segments. À cet effet, les compilateurs créent un en-tête (ou « header ») au début de chaque fichier EXE. Ce header ne sera pas copié en mémoire. Son rôle est simplement d'indiquer au DOS (lors du chargement) la position relative de chaque segment dans le fichier.

    II-B-5. Les cycles de lecture-écriture

    Le code et les variables d'un programme se trouvent dans la mémoire vive. Pour accéder à une donnée en mémoire, le processeur place son adresse dans un bus d'adresse. Un cycle de lecture se met alors en place. Il consiste à retourner la donnée lue au processeur via le bus de données.

    Pour l'écriture, l'adresse de la destination est transmise dans le bus d'adresse et la donnée à écrire est placée dans le bus de données.

    Ouf !

    Nous aurons l'occasion de revenir sur la mémoire vive dans les autres chapitres pour apporter quelques précisions. Vous devriez mieux comprendre toutes ces notions en lisant la suite, car jusqu'ici nous n'avons pas encore parlé de la façon dont l'ordinateur se sert des adresses pour accéder à des données ou pour exécuter du code. C'est là qu'intervient le microprocesseur…

    II-C. LE MICROPROCESSEUR - LES REGISTRES

    II-C-1. Généralités sur les registres

    Le microprocesseur est le cœur de l'ordinateur. C'est lui qui est chargé de reconnaître les instructions et de les exécuter (ou de les faire exécuter). Chaque instruction se présente sous la forme d'une suite de bits qu'on représente en notation hexadécimale (exemple : B44C, ou 1011010001001100 en binaire). Une instruction se compose d'un code opérateur (le plus souvent appelé « opcode ») qui désigne l'action à effectuer (B4 dans notre exemple) et d'un « champ d'opérandes » sur lesquels porte l'opération (ici l'opérande est 4C).

    Pour travailler, le microprocesseur utilise de petites zones où il peut stocker des données. Ces zones portent le nom de registres et sont très rapides d'accès puisqu'elles sont implantées dans le microprocesseur lui-même et non dans la mémoire vive.

    Les registres des processeurs INTEL se classent en quatre catégories :

    • les registres généraux (16 bits) ;
    • les registres de segment (16 bits) ;
    • les registres d'offset (16 bits) ;
    • le registre des indicateurs (16 bits).

    II-C-2. Les registres généraux

    Ils ne sont pas réservés à un usage très précis, aussi les utilise-t-on pour manipuler des données diverses. Ce sont en quelque sorte des registres à tout faire. Chacun de ces quatre registres peut servir pour la plupart des opérations, mais ils ont tous une fonction principale qui les caractérise.

    Nom | Fonction privilégiée
AX | Accumulateur
BX | Base
CX | Compteur
DX | Données

    Le registre AX sert souvent de registre d'entrée-sortie : on lui donne des paramètres avant d'appeler une fonction ou une procédure. Il est également utilisé pour de nombreuses opérations arithmétiques, telles que la multiplication ou la division de nombres entiers. Il est appelé « accumulateur ».

    Exemples d'utilisation :

    • l'instruction “MOV AX, 1982” place l'entier 1982 dans AX ;
    • “ADD AX, 1983” ajoute à AX le nombre 1983 et place le résultat dans AX.

    Le registre BX peut servir de base. Nous verrons plus tard ce que ce terme signifie.

    Le registre CX est utilisé comme compteur dans les boucles. Par exemple, pour répéter 15 fois une instruction en assembleur, on peut mettre la valeur 15 dans CX, écrire l'instruction précédée d'une étiquette qui représente son adresse en mémoire, puis faire un LOOP à cette adresse. Lorsqu'il reconnaît l'instruction LOOP, le processeur « sait » que le nombre d'itérations à exécuter se trouve dans CX. Il se contente alors de décrémenter CX, de vérifier que CX est différent de 0 puis de faire un saut (« jump ») à l'étiquette mentionnée. Si CX vaut 0, le processeur ne fait pas de saut et passe à l'instruction suivante.

    Exemple :

    Image non disponible

    Le registre DX contient souvent l'adresse d'un tampon de données lorsqu'on appelle une fonction du DOS. Par exemple, pour écrire une chaîne de caractères à l'écran, il faut placer l'offset de cette chaîne dans DX avant d'appeler la fonction appropriée.

    Chacun de ces quatre registres comporte 16 bits. On peut donc y stocker des nombres allant de 0 à 65535 en arithmétique non signée, et des nombres allant de -32768 à 32767 en arithmétique signée. Les 8 bits de poids fort d'un registre sont appelés « partie haute » et les 8 autres « partie basse ». Chaque registre est en fait constitué de deux « sous-registres » de 8 bits. La partie haute de AX s'appelle AH, sa partie basse AL. Il en va de même pour les trois autres.

    En arithmétique non signée, il en découle la formule suivante :

    AX = AH x 256 + AL

    ainsi que leurs homologues :

    BX = BH x 256 + BL

    CX = CH x 256 + CL

    DX = DH x 256 + DL

    Il va de soi qu'en modifiant AL ou AH, on modifie également AX.

    Pour stocker un nombre de 32 bits, on peut utiliser des paires de registres. Par exemple, DX:AX signifie DX x 65535 + AX en arithmétique non signée.

    Cette notation (DX:AX) n'est pas reconnue par l'assembleur (ni par la machine). Ne confondez pas cela avec les adresses de segment et d'offset.

    Sur un PC relativement récent, AX, BX, CX, DX ne sont en fait que les parties basses de registres de 32 bits nommés EAX, EBX, ECX, EDX (« E » pour « Extended »). On a donc un moyen plus pratique de stocker les grands nombres. En fait, chaque registre (pas seulement les registres généraux) peut contenir 32 bits.

    II-C-3. Les registres de segment

    Ils sont au nombre de quatre :

    Nom | Nom complet | Traduction
CS | Code segment | Segment de code
DS | Data segment | Segment de données
ES | Extra segment | Extra segment
SS | Stack segment | Segment de pile

    Contrairement aux registres généraux, ces registres ne peuvent servir pour les opérations courantes : ils ont un rôle très précis. On ne peut d'ailleurs pas les utiliser aussi facilement que AX ou BX, et une petite modification de l'un d'eux peut suffire à « planter » le système. Eh oui ! L'assembleur, ce n'est pas Turbo Pascal ! Il n'y a aucune barrière de protection, si bien qu'une petite erreur peut planter le DOS. Mais rassurez-vous : tout se répare très bien en redémarrant l'ordinateur…

    Dans le registre CS est stockée l'adresse de segment de la prochaine instruction à exécuter. La raison pour laquelle il ne faut surtout pas changer sa valeur directement est évidente. De toute façon, vous ne le pouvez pas. Le seul moyen viable de le faire est d'utiliser des instructions telles que des sauts (JMP) ou des appels (CALL) vers un autre segment. CS sera alors automatiquement actualisé par le processeur en fonction de l'adresse d'arrivée.

    Le registre DS est quant à lui destiné à contenir l'adresse du segment des données du programme en cours. On peut le faire varier à condition de savoir exactement pourquoi on le fait. Par exemple, on peut avoir deux segments de données dans son programme et vouloir accéder au deuxième. Il faudra alors faire pointer DS vers ce segment.

    ES est un registre qui sert à adresser le segment de son choix. On peut le changer aux mêmes conditions que DS. Par exemple, si on veut copier des données d'un segment vers un autre, on pourra faire pointer DS vers le premier et ES vers le second.

    Le registre SS adresse le segment de pile. Il est rare qu'on doive y toucher car le programme n'a qu'une seule pile.

    Intéressons-nous à présent aux valeurs que le DOS donne à ces registres lors du chargement en mémoire d'un fichier exécutable ! Elles diffèrent selon que le fichier est un programme COM ou EXE. Pour écrire un programme en assembleur, il est nécessaire de connaître ce tableau par cœur :

    Registre | Valeur avant l'exécution
 | Fichier COM | Fichier EXE
CS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de code
DS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
ES | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
SS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de pile

    Le schéma suivant montre mieux la situation :

    Image non disponible

    Dans un fichier EXE, le header indique au DOS les adresses initiales de chaque segment par rapport au début du programme (puisque le compilateur n'a aucun moyen de connaître l'adresse à laquelle le programme sera chargé). Lors du chargement, le DOS ajoutera à ces valeurs l'adresse d'implantation pour obtenir ainsi les véritables adresses de segment.
    Dans le cas d'un fichier COM, tout est plus simple. Le programme ne comporte qu'un seul segment, donc il suffit tout bêtement au DOS de charger CS, DS, ES et SS avec l'adresse d'implantation.

    Remarque : Pourquoi DS et ES pointent-ils vers le PSP dans le cas d'un fichier EXE ? Première raison : pour que le programmeur puisse accéder au PSP ! Deuxième raison : parce qu'un programme EXE peut comporter un nombre quelconque de segments de données. C'est donc au programmeur d'initialiser ces registres, s'il veut accéder à ses données.

    II-C-4. Les registres d'offset

    Les voici :

    Nom | Nom complet | Traduction
IP | Instruction pointer | Pointeur d'instruction
SP | Stack pointer | Pointeur de pile
SI | Source index | Index de source
DI | Destination index | Index de destination
BP | Base pointer | Pointeur de base

    Le registre IP désigne l'offset de la prochaine instruction à exécuter, par rapport au segment adressé par CS. La combinaison de ces deux registres (i.e. CS:IP) suffit donc à connaître l'adresse absolue de cette instruction. Le processeur peut alors aller la chercher en mémoire et l'exécuter. De plus, il actualise IP en l'incrémentant de la taille de l'instruction en octets. Tout comme CS, il est impossible de modifier IP directement.

    Le registre SP désigne le sommet de la pile. Il faut bien comprendre le fonctionnement de la pile, aussi allons-nous insister sur ce point. La pile ne peut stocker que des mots. On appelle un mot (« word » en anglais) un nombre codé sur deux octets (soit 16 bits). Prenons un exemple simple : un programme COM. Le segment de pile (adressé par SS) et le segment de code ne font qu'un. Avant l'exécution, SP vaut FFFE. C'est l'offset de la dernière donnée de 16 bits empilée, par rapport à SS bien sûr. Pourquoi FFFE ? Tout simplement parce que la pile se remplit à l'envers, c'est-à-dire en partant de la fin du segment et en remontant vers le début. Le premier mot empilé se trouve à l'offset FFFE. Il tient sur deux octets : l'octet FFFE et l'octet FFFF. Mais comment se fait-il qu'un mot soit déjà empilé avant le début du programme ? Ce mot est un zéro que le DOS place sur la pile avant l'exécution de tout programme COM. Nous en verrons la raison plus tard.

    À présent, que se passe-t-il si à un instant quelconque une instruction ordonne au processeur d'empiler un mot ? Eh bien le stack pointer sera décrémenté de 2 et le mot sera copié à l'endroit pointé par SP. Rappelez-vous que la pile se remplit à l'envers ! C'est pour cette raison que SP est décrémenté à chaque empilage et non pas incrémenté.

    Un petit exemple pour rendre les choses plus concrètes :

    PUSH AX

    L'effet de cette instruction est d'empiler le mot contenu dans le registre AX. Autrement dit, SP est automatiquement décrémenté de 2, puis AX est copié à l'adresse SS:SP.

    Lors du dépilage, le mot situé au sommet de la pile, c'est-à-dire le mot adressé par SS:SP, est transféré dans un registre quelconque choisi par le programmeur, après quoi le stack pointer est incrémenté de 2.

    Exemple :

    POP BX

    Cette fois, on retire le dernier mot empilé pour le placer dans le registre BX. Évidemment, SP sera incrémenté de 2 aussitôt après.

    La pile est extrêmement utile lorsqu'il s'agit de stocker provisoirement le contenu d'un registre qui doit être modifié.

    Exemple :

    Image non disponible

    Il est important de comprendre qu'on ne peut dépiler que le mot qui se trouve au sommet de la pile. Le premier mot empilé est le dernier qui sera dépilé. La pile doit être manipulée avec une extrême précaution. Un dépilage injustifié fait planter la machine presque systématiquement.

    Les trois derniers registres sont beaucoup moins liés au fonctionnement interne du processeur. Ils sont mis à la disposition du programmeur qui peut les modifier à sa guise et les utiliser comme des registres généraux. Comme ces derniers cependant, ils ont une fonction qui leur est propre : servir d'index (SI et DI) ou de base (BP). Nous allons expliciter ces deux termes.

    Dans la mémoire, les octets se suivent et forment parfois des chaînes de caractères. Pour utiliser une chaîne, le programmeur doit pouvoir accéder facilement à tous ses octets, l'un après l'autre. Or pour effectuer une opération quelconque sur un octet, il faut connaître son adresse. Cette adresse doit en général être une constante évaluable par le compilateur. Pourquoi une constante ? Parce que l'adresse est un opérande comme les autres, elle se trouve immédiatement après l'opcode et doit donc avoir une valeur numérique fixe !

    Prenons un exemple :

    MOV AH, [MonOctet]

    Pas de panique ! Cette instruction en assembleur signifie « Mettre dans AH la valeur de l'octet adressé par le label MonOctet ! ». À la compilation, « MonOctet » sera remplacé par la valeur numérique qu'il représente et on obtiendra alors une instruction en langage machine telle que :

    8A260601

    8A26 est l'opcode (hexa) de l'instruction “MOV AH, [constante quelconque]”, et 0601 est l'offset de « MonOctet ».

    Il serait pourtant fastidieux, dans le cas d'une chaîne de 112 caractères, de traiter les octets avec 112 instructions dans lesquelles seule l'adresse changerait. Il faudrait pouvoir faire une boucle sur l'adresse, mais alors celle-ci ne serait plus une constante, d'où le problème.

    Pour les constructeurs du microprocesseur, la seule solution était de créer de nouveaux opcodes pour chaque opération portant sur un octet en mémoire. Ces opcodes spéciaux feraient la même action que ceux dont ils seraient dérivés, mais l'adresse passée en paramètre serait alors considérée comme un décalage par rapport à un registre spécial. Il suffirait donc de faire varier ce registre, et le processeur y ajouterait automatiquement la valeur de l'opérande pour obtenir l'adresse réelle ! C'est à cela que servent SI, DI et BP.

    Par exemple :

    MOV AH, [MonOctet + DI]

    sera codé :

    8AA50601

    8AA5 est l'opcode pour l'instruction “MOV AH, [constante quelconque + DI]”.

    Remarque : les registres SI et BP auraient tout aussi bien pu être employés, mais pas les registres généraux, SAUF BX. En effet, BX peut jouer exactement le même rôle que BP. N'oubliez pas que BX est appelé registre de « base », et que BP signifie « Base Pointer. » Nous verrons la différence entre une base et un index lorsque nous commencerons l'assembleur.

    II-C-5. Le registre des indicateurs

    Un programme doit pouvoir faire des choix en fonction des données dont il dispose. Pour cela, il lui faut par exemple comparer des nombres, examiner leur signe, découvrir si une erreur a été constatée, etc.

    Il existe à cet effet de petits indicateurs, les flags qui sont des bits spéciaux ayant une signification très précise. De manière générale, les flags fournissent des informations sur les résultats des opérations précédentes.

    Ils sont tous regroupés dans un registre : le registre des indicateurs. Comprenez bien que chaque bit a un rôle qui lui est propre et que la valeur globale du registre ne signifie rien. Le programmeur peut lire chacun de ces flags et parfois modifier leur valeur directement.

    En mode réel, certains flags ne sont pas accessibles. Nous n'en parlerons pas. Nous ne commenterons que les flags couramment utilisés.

    Nous verrons quelle utilisation on peut faire de ces indicateurs dans la troisième partie de ce tutoriel.

    • CF (« Carry Flag ») est l'indicateur de retenue. Il est positionné à 1 si et seulement si l'opération précédemment effectuée a produit une retenue. De nombreuses fonctions du DOS l'utilisent comme indicateur d'erreur : CF passe alors à 1 en cas de problème.
    • PF (« Parity Flag ») renseigne sur la parité du résultat. Il vaut 1 si ce dernier est pair.
    • AF (« Auxiliary Carry Flag ») est peu utilisé.
    • ZF (« Zero Flag ») passe à 1 si le résultat d'une opération est égal à zéro.
    • SF (« Sign Flag ») passe à 1 si le résultat d'une opération sur des nombres signés est négatif.
    • TF (« Trap Flag ») est utilisé pour le « débogage » d'un programme. S'il vaut 1, une routine spéciale du débogueur est appelée après l'exécution de chaque instruction par le processeur.
    • IF (« Interrupt Flag ») sert à empêcher les appels d'interruptions lorsqu'il est positionné à 1. Cependant, toutes les interruptions ne sont pas « masquables ».
    • DF (« Direction Flag ») est utilisé pour les opérations sur les chaînes de caractères. S'il vaut 1, celles-ci seront parcourues dans le sens des adresses décroissantes, sinon les adresses seront croissantes.
    • OF (« Overflow Flag ») indique qu'un débordement s'est produit, c'est-à-dire que la capacité de stockage a été dépassée. Il est utile en arithmétique signée. Avec des nombres non signés, il faut utiliser ZF et SF.

    Remarque : Les notations CF, PF, AF, etc. ne sont pas reconnues par l'assembleur. Pour utiliser les flags, il existe des instructions spécifiques que nous décrirons plus tard.

    II-D. LES INTERRUPTIONS

    II-D-1. Introduction

    Le microprocesseur ne peut exécuter qu'une seule instruction à la fois. Pour connaître son adresse, il utilise le couple de registres CS:IP dont la valeur est incrémentée automatiquement. Par conséquent, le code du programme courant est exécuté de manière linéaire.

    Imaginons cependant qu'un événement extérieur demande l'attention de l'ordinateur, par exemple la pression d'une touche du clavier. La machine doit pouvoir réagir immédiatement, sans attendre que le programme en cours d'exécution se termine. Pour cela, elle interrompt ce dernier pendant un bref instant, le temps de traiter l'événement survenu puis rend le contrôle au programme interrompu.

    Une interruption n'est rien d'autre que l'appel d'une routine spéciale présente en mémoire appelée ISR (« Interrupt Service Routine »).

    Les interruptions se divisent en trois catégories :

    • les interruptions électroniques, par exemple : le clavier ;
    • les interruptions du BIOS, par exemple : l'accès aux disques ;
    • les interruptions du DOS, par exemple : l'accès aux systèmes de fichiers.

    Comment sont-elles déclenchées ?

    Une interruption peut être déclenchée par votre matériel. C'est ce qui arrive lorsque vous appuyez sur une touche du clavier. Aucun logiciel n'intervient et le contrôle est passé directement à la routine qui gère le clavier : ce sont les interruptions matérielles.

    Les interruptions logicielles sont quant à elles appelées par des instructions en langage machine au sein d'un programme. Leur importance est capitale. Rappelez-vous que contrairement au PASCAL ou au C, l'assembleur ne dispose pas de fonction préprogrammée. Chaque instruction doit être directement traduisible en langage machine.

    Mais alors, comment fait-on pour écrire une chaîne de caractères à l'écran ? Ou bien pour lire un caractère entré au clavier ?

    Eh bien on le fait de la même façon que le DOS lui-même ! On déclenche les interruptions appropriées à l'aide de l'instruction « INT » du langage machine. C'est donc une routine du DOS (ou parfois du BIOS) qui fera tout le travail. Les paramètres (ou leurs adresses) sont passés dans les registres.

    Voici un petit exemple en assembleur qui écrit la lettre ‘A' à l'écran :

    Image non disponible

    Examinons-le ligne par ligne :

    • l'instruction « MOV DL, ‘A' » demande au processeur de mettre dans le registre DL le code ASCII de la lettre ‘A', c'est-à-dire 65, ou 41h ;
    • « MOV AH, 02 » : mettre le nombre 2 dans AH ;
    • enfin, la dernière instruction appelle l'interruption numéro 21h. Il existe 256 interruptions. Toutes sont notées en base hexadécimale.

    Explications :

    Comme vous aurez très vite l'occasion de vous en rendre compte, l'interruption 21h est l'interruption du DOS par excellence. Elle permet d'appeler de nombreuses fonctions très diverses. Pour cela, il suffit de mentionner leur numéro dans le registre AH.

    Il est très difficile de mémoriser le rôle de chaque interruption, et a fortiori de chaque fonction ou sous-fonction, d'autant plus qu'elles sont désignées par des numéros hexadécimaux et qu'elles attendent des paramètres dans des registres précis. C'est pourquoi tout programmeur se doit d'avoir à sa disposition une liste des interruptions pour travailler. Celle de Ralph Brown est très connue, et vous la trouverez sur l'Internet.

    Revenons à notre exemple. La fonction numéro 2 de l'interruption 21h sert à écrire un caractère à l'écran. Il faut pour cela écrire le code ASCII du caractère dans le registre DL et bien sûr placer le nombre 2 dans AH.

    Une fois que AH et DL ont été ajustés, l'interruption 21h peut être appelée à l'aide de l'instruction INT.

    D'autres interruptions ne remplissent qu'une seule tâche. Vous n'avez donc pas besoin de mettre un numéro de fonction dans AH. L'appel de l'interruption suffit.

    II-D-2. La table des vecteurs d'interruptions

    À chaque appel d'interruption, l'ordinateur doit pouvoir trouver l'adresse de l'ISR correspondante. Pour cela, il dispose de la table des vecteurs d'interruptions (TVI, ou IVT : « Interrupt Vector Table »). Cette table est implantée à l'adresse 0000:0000 c'est-à-dire au début de la RAM.

    La table est organisée comme suit :

    Adresse | Taille (octets) | Valeur
0000:0000 | 2 | Adresse d'offset de l'interruption numéro 0
0000:0002 | 2 | Adresse de segment de l'interruption numéro 0
0000:0004 | 2 | Adresse d'offset de l'interruption numéro 1
0000:0006 | 2 | Adresse de segment de l'interruption numéro 1
0000:0008 | 2 | Adresse d'offset de l'interruption numéro 2
0000:000A | 2 | Adresse de segment de l'interruption numéro 2
0000:000C | 2 | Adresse d'offset de l'interruption numéro 3
0000:000E | 2 | Adresse de segment de l'interruption numéro 3
etc. etc. etc.
0000:03FC | 2 | Adresse d'offset de l'interruption numéro 255
0000:03FE | 2 | Adresse de segment de l'interruption numéro 255

    La raison pour laquelle les offsets précèdent les adresses de segment vient du codage en « little endian » utilisé par INTEL. Les processeurs de cette marque (contrairement à la plupart des autres, qui travaillent en « big endian ») ont une représentation des données en mémoire aussi curieuse qu'insupportable pour le programmeur : ils placent le poids fort après le poids faible. Ainsi, le mot 4A28h sera codé 284Ah. Puisque l'adresse de l'ISR tient sur 32 bits (16 + 16), elle est représentée comme un double-mot (« dword »), donc l'adresse d'offset, qui est le mot de poids faible, se trouve au début. Eh oui, c'est pénible, mais il faudra s'y faire !

    Remarque : pour calculer l'adresse de l'entrée dans la TVI correspondant à l'interruption numéro X, il suffit de multiplier X par 4.

    Exemple : l'adresse de l'ISR numéro 21h est stockée à 0000:0084.

    II-D-3. Sauvegarde de l'état des registres lors de l'appel

    Un appel d'interruption obéit à certaines règles, car il est indispensable, une fois l'ISR exécutée, que le programme interrompu retrouve les registres dans le même état qu'ils étaient auparavant. Tout doit se passer comme si l'interruption n'avait jamais eu lieu. C'est pourquoi avant de faire un saut à l'ISR pointée par l'entrée correspondante dans la TVI, l'ordinateur sauvegarde tous les registres sur la pile courante. Il restaurera leur contenu avant de rendre le contrôle. Cette procédure est automatique. Il n'incombe pas au programmeur de prendre toutes ces précautions.

    III. DEUXIÈME PARTIE

    Cette partie a pour but de vous présenter l'architecture du langage assembleur à travers de courts exemples. Ne vous inquiétez pas si vous ne comprenez pas parfaitement certaines instructions : toutes seront détaillées dans la troisième partie. L'essentiel est que vous compreniez grossièrement ce qu'on fait et pourquoi on le fait . Les explications qui accompagnent ces exemples devraient y suffire. Vous pourrez ensuite apprendre le langage en lui-même en consultant la partie suivante.

    Remarques pratiques préliminaires

    • Les mots qui sont imprimés en italiques sont ceux qui ne font pas partie du langage en lui-même. Ils sont choisis par le programmeur.
    • Tous les programmes présentés ci-dessous suivent la syntaxe du compilateur TASM. Il existe de très légères différences d'un compilateur à l'autre.
    • Le code peut être écrit en majuscules ou en minuscules. Chaque mot est séparé des autres par des espaces ou des tabulations.
    • Les commentaires sont précédés du signe ‘;'.
    • Les nombres doivent toujours commencer par un chiffre (entre 0 et 9), même les nombres hexadécimaux. Il faut donc écrire 0F2Ah et non pas F2Ah. Par contre, l'écriture 5CFh est correcte.
    • Les apostrophes et les guillemets sont équivalents. Il est cependant plus commode de réserver l'usage des guillemets aux chaînes de plusieurs caractères et celui des apostrophes aux caractères isolés.
    • Vous pouvez taper vos programmes avec l'éditeur de texte du DOS (EDIT.COM). Donnez-leur l'extension ‘.asm'.
    • La compilation se fait en tapant “TASM /m9 MONPROG” si votre source s'appelle ‘MONPROG.ASM'.

    Le paramètre ‘/m9', facultatif, indique au compilateur qu'il devra effectuer 9 passes, c'est-à-dire qu'il examinera 9 fois le code source. Comme chaque examen se fait de manière linéaire, le compilateur trouve parfois des instructions qui font référence à des labels placés plus loin dans le programme. Il manque donc d'informations, mais il continue son examen jusqu'à la fin. Quand il a terminé, il recommence tout depuis le début pour résoudre les problèmes qui s'étaient posés.

    La compilation crée un fichier objet (‘.obj'). Pour obtenir un fichier EXE, tapez ”TLINK MONPROG”. Pour un fichier COM, tapez “TLINK /tdc MONPROG”.

    Après l'édition des liens (le « linkage »), vous pouvez supprimer les fichiers MONPROG.obj' et ‘MONPROG.map'.

    • Il est possible de créer un fichier BAT qui s'occupe de toutes ces étapes. Ouvrez l'éditeur EDIT du DOS et tapez un programme tel que celui-ci :

    Enregistrez-le et nommez-le MAKE.BAT. Vous pouvez compiler et linker en tapant “MAKE MONPROG”. Pour compiler des fichiers COM, ajoutez le paramètre '/tdc' à la commande TLINK.

    Sélectionnez

    @ECHO OFF
    TASM /m9 %1.asm
    IF NOT EXIST %1.obj GOTO FIN
    TLINK %1.obj REM : ajouter ici /tdc pour obtenir un fichier COM
    ERASE %1.map
    ERASE %1.obj
    :FIN
    • Pour pouvoir lancer TASM et TLINK quel que soit le dossier courant, changez la variable PATH ainsi :

    PATH = %path%;c:\MonChemin\DossierTASM

    Vous pouvez inclure cette ligne à la fin de votre fichier AUTOEXEC.BAT afin qu'elle soit exécutée à chaque démarrage.

    • Vous pouvez déboguer vos programmes (par exemple les exécuter instruction par instruction en observant les changements induits dans la RAM et dans les registres…) avec le logiciel Turbo Debugger 16/32 bits qui est très pratique et très performant. À défaut, vous pouvez vous rabattre sur l'archaïque DEBUG.COM (il se trouve dans votre dossier de commandes DOS). Mais alors bon courage !

    Le seul travail du compilateur (et du linkeur) est de convertir chacune de vos instructions en son équivalent en langage machine. Le programme compilé présentera donc exactement la même structure et la même linéarité que votre code source .

    III-A. PREMIER EXEMPLE : LES FICHIERS COM

    Voici un petit programme COM qui écrit le message « Bonjour, monde ! » à l'écran.

    Image non disponible

    Ce code source commence par des directives. Une directive est une information que le programmeur fournit au compilateur. Elle n'est pas transformée en une instruction en langage machine. Elle n'ajoute donc aucun octet au programme compilé.

    La directive “.386“ indique au compilateur que le programme est destiné à tourner sur des processeurs INTEL de modèle 386 (ou supérieur). Cela nous autorise à utiliser certaines instructions qui ne sont pas disponibles sur les modèles antérieurs, comme PUSHA ou POPA. Dans cet exemple, cette directive aurait très bien pu être omise.

    La ligne

    code segment use16

    sert à déclarer un segment que l'on appelle “code”. On aurait tout aussi bien pu le nommer “marteau” ou “voiture”. Ce sera le segment de notre programme. N'oubliez pas qu'un fichier COM ne peut comporter qu'un seul segment. Cette ligne ne sera pas compilée : elle ne sert qu'à indiquer au compilateur le début d'un segment.

    Le mot “use16” indique que les adresses de segment et d'offset sont codées sur 16 bits et non sur 8 bits. Vous devez systématiquement l'écrire.

    La directive

    assume cs:code, ds:code, ss:code

    informe le compilateur que tout au long du programme, CS, DS et SS pointeront de façon privilégiée vers le segment “code”, ce qui est évident d'ailleurs puisque c'est le seul segment… Vous comprendrez le sens exact de “assume” dans la troisième partie du cours.

    Enfin, les mots

    org 100h

    signifient qu'il faudra ajouter 100h (soit 256) à tous les offsets. Pourquoi ? Souvenez-vous de la structure d'un programme COM en mémoire. Si vous n'écrivez pas cette ligne, TASM considérera que le programme débute à l'offset 0000. Or, lors de l'exécution, le DOS le chargera après le PSP, c'est-à-dire à l'adresse 100h. C'est pourquoi il est nécessaire de recalculer les offsets : l'offset 0000 deviendra 0100. Cette directive est en quelque sorte le trait caractéristique des fichiers COM.

    Nous en arrivons au programme proprement dit. Il commence par un label :

    debut :

    Lui non plus n'est pas compilé. Il ne sert qu'à représenter l'adresse de l'instruction qui le suit, c'est-à-dire :

    mov ah, 09h

    Cette ligne demande au processeur de charger la valeur 9 dans le registre AH. C'est le numéro de la fonction de l'interruption 21h qui écrit une chaîne de caractères à l'écran. L'offset de cette chaîne est attendu dans DX. D'où la ligne suivante :

    mov dx, offset message

    Le mot-clé “offset” sert à extraire l'offset du label “message” qui représente quant à lui l'adresse du message à écrire (il contient donc une adresse de segment ET un offset). L'adresse de segment de la chaîne doit être transmise dans DS. Mais il est inutile de changer ce registre, car il pointe déjà vers notre segment.

    Il nous reste à appeler l'interruption 21h :

    int 21h

    et à rendre la main au DOS :

    ret

    Lorsque nous aborderons les procédures, vous comprendrez mieux le sens de ce mot et ce qu'il fait exactement. Pour l'instant, sachez simplement que seul un fichier COM peut se terminer avec cette instruction.

    Nous arrivons à la ligne :

    message db “Bonjour, monde !”, ‘$'

    Il s'agit d'une définition de données. Le mot “db” (« define byte ») signifie que le compilateur devra écrire les octets qui suivent tels qu'ils sont dans notre code source. Il va donc écrire le code ASCII du ‘B', puis celui du ‘o', etc. Il terminera en écrivant le code ASCII du signe ‘$'. C'est ainsi que la fonction 9 de l'interruption 21h reconnaît la fin de la chaîne à écrire. Si vous oubliez ce signe, elle écrira tous les octets de la RAM jusqu'à ce qu'elle tombe par hasard sur lui.

    Le mot “message” placé en début de ligne est un label de données. Il représente l'adresse du code ASCII du ‘B'. Remarquez qu'il n'y a pas de caractère ‘:' après un label de données.

    La ligne

    code ends

    indique la fin du segment « code », et enfin

    end debut

    informe le compilateur que le fichier est fini, tout comme le “END.” du PASCAL. Le nom du label « debut » est mentionné : ce sera le point d'entrée de notre programme. C'est vers lui que pointera CS:IP avant l'exécution.

    Remarque : vous n'êtes pas tenu de rendre aux registres la valeur qu'ils avaient au début de votre programme. De toute façon, avant de charger un programme, le DOS sauvegarde le contenu de tous les registres puis met le contenu des registres généraux (ainsi que SI, DI et BP) à zéro. Il les restaurera quand vous lui rendrez la main.

    III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE

    Le programme suivant est un programme EXE qui fait exactement la même chose que l'exemple précédent.

    Image non disponible

    Examinons-le !

    Tout d'abord, la directive

    org 100h

    a disparu.

    En effet, lors de l'exécution, CS pointera vers notre segment de code et non pas vers le PSP. Il est donc inutile de décaler les offsets de 256.

    Remarquons que notre programme dispose de deux segments supplémentaires :

    • le segment « data » est destiné à contenir les données, c'est-à-dire les variables ;
    • le segment « pile » sera notre segment de pile.

    Notre directive « assume » devient donc :

    assume cs:code, ds:data, ss:pile

    Ainsi, le compilateur est informé que DS pointera vers le segment « data » et SS vers « pile ».

    Les deux lignes suivantes,

    mov ax, data

    mov ds, ax

    servent à initialiser le registre DS. Celui-ci pointe vers le PSP au début du programme, mais nous voulons le faire pointer vers notre segment de données appelé « data ». Cela est nécessaire puisque la fonction 9 de l'interruption 21h attend l'adresse de la chaîne dans le couple DS:DX et que notre message se trouve dans le segment de données.

    La première instruction charge l'adresse du segment « data » dans AX. La seconde transfère cette valeur de AX dans DS.

    Mais pourquoi diable utiliser AX comme intermédiaire ? Après tout, on pourrait écrire :

    mov ds, data

    Eh bien non ! Pour la simple raison que DS est un registre de segment et qu'en tant que tel on ne peut pas lui charger de valeur immédiate.

    On appelle « valeur immédiate » toute constante tapée directement dans l'instruction elle-même.

    Exemples de chargement de valeurs immédiates :

    mov ax, 135 ;charge 135 dans AX

    mov bx, offset message ;charge l'offset de message dans BX

    mov bx, offset fin - offset debut ;charge le nombre d'octets entre fin et debut dans BX

    mov es, 10 ;instruction illicite, car ES est un registre de segment !

    Remarque : une autre possibilité aurait été d'écrire : “PUSH AX” (empiler AX) puis “POP DS” (dépiler le dernier nombre empilé et le placer dans DS).

    Les trois lignes suivantes :

    mov ah, 09h

    mov dx, offset message

    int 21h

    n'ont pas changé.

    Il nous faut également terminer le programme par un appel de la fonction 4ch de l'interruption 21h. C'est ainsi que se terminent les programmes EXE.

    mov ah, 4ch

    int 21h

    Remarque : on aurait également pu écrire :

    mov ax, 4c00h

    int 21h

    La seule différence est que AL est mis à zéro, ce qui indique au programme à qui on rend le contrôle (ici le DOS) que notre programme s'est terminé normalement. Les fichiers COM peuvent également utiliser la fonction 4ch.

    Le segment de code se termine :

    code ends

    et le segment de données commence à sa suite :

    data segment use16

    message db “Bonjour, monde !”, ‘$'

    data ends

    Il nous reste à écrire le segment de pile. Dans ce programme, il n'était pas absolument indispensable de le séparer du segment de code. Mais c'est une bonne habitude de le faire.

    pile segment stack

    remplissage db 256 DUP (?)

    pile ends

    Le mot-clé « stack » indique que ce segment est le segment de pile.

    Les mots « db 256 DUP (?) » déclarent 256 octets non initialisés. C'est la « matière » de notre pile.

    Sachez que tout appel d'interruption se traduit par l'empilage des flags et de CS:IP. Il est donc indispensable d'avoir une pile, même si celle-ci peut éventuellement partager le même segment que le code, comme dans un fichier COM. Mais dans un programme EXE, il vaut mieux réserver un segment à la pile. Les 256 octets que nous déclarons ici indiquent seulement que la pile contient 256 octets. Ainsi, au début de l'exécution, SS:SP pointera vers la fin de ces octets. Nous verrons ce que signifie le point d'interrogation dans la troisième partie.

    La fin du fichier et le point d'entrée sont signalés par :

    end debut

    Et voilà ! Vous avez découvert l'allure d'un programme en assembleur. Nous pouvons donc à présent nous pencher sur l'étude du langage.

    IV. TROISIÈME PARTIE

    IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE

    IV-A-1. Les définitions de données

    Les mots “db” (« define byte »), “dw” (« define word »), ”dd” (« define double word ») permettent de déclarer et d'initialiser une variable. Il faut bien comprendre que leur seule action est d'écrire les données dans l'exécutable à l'endroit même où elles se trouvent dans le code source.

    Pour accéder à ces données (appelées « variables »), il suffit de connaître leur adresse. Pour cela, on peut les faire précéder d'un label de données.

    Exemple :

    TOTO dw 1982

    Dans la ligne précédente, TOTO représente l'adresse du nombre 1982 qui est défini juste après. Comme ce nombre est codé sur deux octets (word = 16 bits), c'est le premier octet qui est adressé par TOTO.

    Le compilateur se contentera d'écrire le nombre 1982 à l'endroit où se trouve la déclaration.

    Remarque : en C, on écrirait ceci :

    int TOTO = 1982;

    En BASIC ou en PASCAL, cette définition n'a pas d'équivalent, puisque les variables ne peuvent être initialisées lors de leur déclaration. Il est donc obligatoire d'ajouter une ligne de code pour le faire.

    Dans la partie précédente, nous avons eu à définir le message qui serait affiché à l'écran. Voici la ligne que nous avons écrite :

    message db “Bonjour, monde !”, ‘$'

    Lorsqu'il rencontre une chaîne de caractères, le compilateur écrit les codes ASCII de tous les caractères, les uns à la suite des autres.

    Voici une autre manière d'écrire cette définition de données. Le résultat (i.e. le programme compilé) est EXACTEMENT LE MÊME :

    message db 42h, 111, ‘nj'

    db ‘our, mo'

    db ‘nde !$'

    Pour définir plusieurs fois à la suite les mêmes données, on utilise “DUP” (« duplicate ») de la manière suivante :

    TOTO db 100 dup(“TOTO EST BEAU”)

    Cela revient à écrire :

    TOTO db “TOTO EST BEAU”

    TOTO db “TOTO EST BEAU”

    TOTO db “TOTO EST BEAU”

    ;… etc. (100 fois) …

    Il est fréquent que de nombreuses données n'aient pas besoin d'être initialisées à une valeur précise. Dans ce cas, on les regroupe à la fin du programme et on les remplace par le caractère ‘?'. Les adresses des labels seront calculées de la même façon, mais aucune donnée ne sera écrite dans le fichier (et a fortiori dans la RAM). On dit que l'on met ses variables sur le « tas » (« heap » en anglais). La seule différence avec une définition classique est qu'au début de l'exécution, nos variables n'auront pas de valeur définie. Leurs valeurs seront « aléatoires » en ce sens qu'on ne peut les connaître au moment de la compilation.

    Exemple :

    Image non disponible

    Remarque : Si elles ne sont pas regroupées en fin de programme, le compilateur sera obligé d'écrire les données dans le fichier afin de ne pas fausser les adresses des variables (ou du code) qui suivent. Il remplira alors les points d'interrogation avec des zéros.

    Pour indiquer qu'un nombre est noté en base hexadécimale, on lui ajoute la lettre ‘h'. La lettre ‘b' signifie que le nombre est codé en binaire, la lettre 'o' en octal et la lettre ‘d' en base décimale (base par défaut).

    IV-A-2. L'adressage

    IV-A-2-a. L'adressage immédiat

    On appelle « adressage immédiat » l'adressage qui ne fait intervenir que des constantes.

    Considérons cette partie de programme qui stocke le nombre 125h dans une variable appelée TOTO, lui ajoute 15 puis charge le résultat dans AX.

    Image non disponible

    La ligne

    mov word ptr ds:[TOTO], 125h

    charge le nombre 125h dans le mot de la RAM adressé par DS et l'offset de “TOTO”. La ligne suivante ajoute 15 au contenu de ce mot.

    L'expression “word ptr” devant l'adresse, obligatoire ici, indique la taille de la variable dans laquelle doit être stocké le nombre 125h.

    Si on avait mis “dword ptr”, ce nombre aurait été codé sur 32 bits (00000125h) au lieu de 16 bits (0125h) : on aurait donc écrasé les deux octets qui suivent la variable. Si on avait mis “byte ptr”, la compilation aurait été impossible, car un octet ne peut contenir un nombre supérieur à FFh.

    Le compilateur n'a en effet aucun moyen de connaître cette taille. La variable “TOTO” n'a pas de taille (malgré le mot “dw”) : ce n'est en fait qu'un pointeur vers le premier octet du word qu'elle représente.

    En revanche, la troisième instruction ne fait pas apparaître l'expression “word ptr”. Cette fois-ci elle est facultative du fait que la destination (AX = 16 bits) impose la taille.

    On peut évidemment remplacer “TOTO” par une constante numérique :

    mov word ptr ds:[004Ch], 125h

    Le mot 125h sera alors écrit à l'adresse DS:4C.

    Une question se pose à présent : que se passe-t-il si le programmeur n'écrit pas le registre de segment dans l'adresse ?

    C'est là qu'intervient la directive “assume”. Le compilateur va devoir trouver lui-même quel registre l'utilisateur a voulu sous-entendre.

    Si on a utilisé un label, alors le segment sera celui dans lequel est déclaré le label. Mais le compilateur veut un REGISTRE de segment. Il va donc prendre celui qui est censé pointer vers le bon segment et pour le savoir, il examine la directive assume.

    Voilà pourquoi cette dernière peut nous épargner d'écrire pour chaque variable l'expression “ds:”.

    Comprenez bien que cette directive ne sert à rien d'autre qu'à cela et qu'en aucune façon elle ne force les registres de segment à prendre quelque valeur que ce soit.

    IV-A-2-b. L'adressage indexé et/ou basé

    Comme nous l'avons déjà expliqué, il est possible d'utiliser des registres de base ou d'index pour adresser un octet.

    On appelle « base » les registres BX et BP et « index » les registres SI et DI. La différence est que la base est censée être fixe tandis que l'index varie automatiquement lorsqu'on utilise certaines opérations, telles que MOVSB.

    Remarque : BX, BP, DI et SI peuvent tous être utilisés comme des registres généraux.

    Voici les adressages possibles :

    • Constante seule (= adressage immédiat) :

    Exemples :

    MOV AH, byte ptr [TOTO]

    MOV BX, word ptr ds:[1045h]

    MOV ES:[10 + TOTO x 2], BL

    • Base seule :

    Exemple :

    MOV dword ptr ds:[BP], 15

    • Index seul :

    Exemple :

    MOV dword ptr es:[DI], 142

    • Base + Constante :

    Exemples :

    MOV byte ptr ds:[BP + 1], 12

    MOV ds:[BX + (TOTO - BOBO)/10], AX

    • Index + Constante :

    Exemples :

    MOV CX, [TOTO + DI]

    MOV AL, ds:[1 + SI]

    • Base + Index :

    Exemple :

    MOV ds:[BP + SI], AH

    • Base + Index + Constante :

    Exemple :

    MOV AX, word ptr [TOTO + BX + DI + 1]

    Remarque : si la constante n'est pas un label, il est parfois impératif de spécifier le registre de segment ! Tout dépend du contexte…

    IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS

    IV-B-1. Les sauts inconditionnels

    Ce paragraphe répond à la question : « Mais comment fait-on un GOTO en assembleur ? ».

    L'instruction équivalente est JMP qui signifie « jump ».

    La syntaxe est

    JMP MonLabel

    La référence à l'étiquette MonLabel sera remplacée lors de la compilation par la distance (signée) en octets qui sépare l'instruction qui suit immédiatement le JMP de l'instruction adressée par MonLabel. En pratique, le JMP s'utilise exactement comme un GOTO en BASIC.

    Cette instruction permet de faire un saut inconditionnel : le programme fera ce saut quoi qu'il arrive.

    Exemple :

    Image non disponible

    Le mot “short” ajouté après “jmp” indique au compilateur que le label “COUCOU” se trouve à une distance (signée) qui peut être stockée sur un seul octet. Le compilateur ne le sait pas encore lorsqu'il essaie de compiler l'instruction de saut, car le label se trouve en deçà de cette instruction. C'est pourquoi il prévoit deux octets pour écrire le saut, au cas où l'adresse d'arrivée est éloignée de plus de 128 octets. Lorsqu'il effectue une deuxième « passe », s'il s'aperçoit qu'un seul octet aurait suffi il remplit alors l'octet inutile avec l'instruction NOP (« No Operation ») qui ne fait rien du tout. Le programme marchera parfaitement (encore heureux !), mais cela gaspille un octet. Le programmeur a la possibilité d'aider le compilateur en ajoutant le mot “short”, ainsi un seul octet est prévu pour la distance de saut. Si le label est placé plus haut que l'instruction de saut, il est inutile de l'écrire.

    Remarque : Ce mot peut également être utilisé avec les instructions de saut conditionnel que nous étudierons plus loin.

    IV-B-1-a. Les procédures

    IV-B-1-a-i. appels de procédures

    Ceux qui ont déjà programmé en BASIC connaissent les instructions GOSUB et RETURN. Leurs équivalents en assembleur sont CALL et RET.

    • L'instruction CALL sert à appeler une procédure :

    Syntaxe :

    CALL MonLabel

    Action : empile l'offset de l'instruction suivante puis fait un simple saut à l'adresse représentée par MonLabel.

    • RET (ou RETN) permet de retourner à l'instruction qui suit immédiatement le CALL :

    Syntaxe :

    RET

    Action : dépile l'adresse de retour et la met dans IP. Le programme continue donc à l'adresse qui suit le CALL.

    Remarque : Rappelez-vous qu'il est possible de terminer un programme COM avec l'instruction RET. Puisque le DOS empile un zéro de deux octets au chargement du programme, IP prendra la valeur 0000 lorsque le processeur exécutera cette instruction : il pointera donc vers le début du PSP. Or le PSP commence toujours par les deux octets suivants : CDh 20h (ce qui s'écrit INT 20h en assembleur). L'interruption 20h, qui permet de terminer un programme COM, sera donc appelée et le contrôle sera rendu au DOS.

    Voici un exemple d'utilisation des procédures aussi simple que possible : ce programme COM appelle 12 fois une procédure qui écrit un message à l'écran et rend la main au DOS. Il n'est d'aucune utilité et n'est pas optimisé du tout.

    Image non disponible

    Remarque : Les codes ASCII 10 et 13 représentent respectivement la fin de ligne et le retour chariot. Grâce à eux, on revient à la ligne chaque fois qu'on a écrit le message.

    Ce programme fonctionne parfaitement. Toutefois, les conventions d'écriture veulent que les procédures soient écrites comme suit :

    Image non disponible

    Le programme compilé est toujours le même, mais le code est plus lisible puisqu'on distingue les procédures des simples labels. Le mot “near” signifie que l'adresse de cette procédure est réduite à un offset. Il n'y a pas d'adresse de segment. La procédure ne pourra donc être appelée que de l'intérieur même du segment.

    Le mot qui a le sens contraire de “near” est “far”. Les « call far » sont assez peu utilisés.

    IV-B-1-a-ii. le passage des paramètres

    Il existe deux moyens de passer des paramètres à une procédure : les registres et la pile.

    • Le passage par registres :

    Il consiste à transmettre les paramètres dans des registres convenus à l'avance, par exemple AX et BX. Les deux gros avantages de cette méthode sont sa simplicité et la vitesse d'exécution. L'inconvénient majeur est le nombre limité de registres, d'autant plus qu'au moment de l'appel, certains d'entre eux ne seront peut-être pas disponibles.

    • Le passage par la pile :

    C'est la méthode qu'utilisent les langages de haut niveau tels que le C ou le PASCAL. Avant l'appel, on empile les paramètres un à un. La procédure appelée se chargera de les lire.

    Pour cela on utilise le registre BP de la manière suivante : on transfère la valeur de SP dans BP à l'aide de l'instruction “MOV BP, SP”. L'adresse de retour (qui sera dépilée quand le processeur rencontrera l'instruction “RET”) se trouve à l'adresse SS:[BP], le dernier paramètre est adressé par SS:[BP + 2], l'avant-dernier par SS:[BP + 4], etc. On peut ainsi lire n'importe quel paramètre sans le dépiler.

    Attention cependant ! À la fin de la procédure, il faudra tout de même rendre à SP la valeur qu'il avait avant l'empilage des paramètres. Pour cela, on fait suivre l'instruction RET du nombre de paramètres, multiplié par 2 (car chaque paramètre comporte deux octets).

    Exemple :

    Image non disponible

    Soyez toujours très vigilant avec les appels de procédures : pensez que l'adresse de retour sera dépilée lorsque le programme rencontrera l'instruction RET. Il serait donc suicidaire de laisser une donnée empilée avant d'appeler cette instruction.

    IV-B-1-b. Les macros

    Étant donné que certaines instructions se répètent constamment dans un programme, l'écriture de macrofonctions (ou macros) est un moyen pratique de rendre votre code source plus lisible.

    Il est possible de choisir pour certaines suites d'instructions un nom qui les représente. Lorsque le compilateur (en fait, le préprocesseur) rencontrera ce nom dans votre code source, il le remplacera par les lignes de code qu'il désigne. Ces lignes forment une « macro ».

    Les macros, à la différence des procédures, n'ont aucune signification pour la machine. Seul le compilateur comprend leur signification. Elles ne sont qu'un artifice mis à la disposition du programmeur pour clarifier son programme. Lorsque le compilateur rencontre le nom d'une macro dans votre code, il le remplace par le code de la macro. Tout se passe exactement comme si vous aviez tapé vous-même ce code à la place du nom de la macro.

    Ainsi, si vous appelez quinze fois une macro dans votre programme, le compilateur écrira quinze fois le code de cette macro. C'est toute la différence avec les procédures qui ne sont écrites qu'une seule fois, mais peuvent être appelées aussi souvent qu'on veut à l'aide d'un CALL.

    Voici comment écrire une macro : l'exemple suivant sert à écrire un message à l'écran.

    Image non disponible

    Le code précédent peut être écrit n'importe où dans votre programme, à condition qu'il se trouve avant tout appel de cette macro. Afin d'éviter les ennuis, il est fortement conseillé de réunir vos macros au début du code, avant toute autre ligne. De toute façon, il ne sera pas compilé à l'endroit où vous l'avez écrit, mais aux endroits où se trouvent les appels de macros.

    Le mot “text?” est un « paramètre ». Le point d'interrogation n'est pas requis ; nous l'avons mis pour indiquer qu'il s'agit d'un paramètre et non d'une variable. Une macro peut utiliser plusieurs paramètres séparés par des virgules.

    Pour appeler cette macro, il vous suffit d'écrire la ligne

    ecrit_texte “Coucou ! Ceci est un essai !”

    Le compilateur se chargera alors de la remplacer par les instructions comprises entre la première et la dernière ligne de cet exemple, en prenant le soin de remplacer le mot “text?” par le message fourni en paramètre.

    En assembleur, chaque label doit avoir un nom unique. Or, si une macro est appelée plusieurs fois, les mêmes noms seront utilisés. Il faut donc la plupart du temps inclure la directive LOCAL qui forcera le compilateur à changer le nom des labels à chaque appel de la macro. Attention : cette directive doit suivre immédiatement la déclaration de la macro.

    Supposons à présent que l'on veuille écrire à l'écran le message « Je suis bien content » et revenir à la ligne à l'aide de notre macro ecrit_texte.

    La syntaxe suivante :

    ecrit_texte “Coucou ! Ceci est un essai !”, 10, 13

    est incorrecte, car le compilateur croirait que l'on a écrit trois paramètres ! Il faut alors entourer notre unique paramètre par les signes ‘<' et ‘>' :

    ecrit_texte <”Coucou ! Ceci est un essai !”, 10, 13>

    IV-B-1-c. La directive EQU

    La directive EQU a un rôle voisin de celui des macros. Elle permet de remplacer un simple mot par d'autres plus complexes. Son intérêt est qu'elle peut être invoquée en plein milieu d'une ligne.

    Quelques exemples :

    Longueur EQU (fin - debut)

    Message EQU “Bonjour messieurs ! Comment allez-vous ?”, ‘$'

    Version EQU 2

    Quitter EQU ret

    Quitter2 EQU int 20h

    Mettre_dans_AH EQU mov ah,

    Interruption_21h EQU int 21h

    Un programme qui contient de telles directives peut se terminer ainsi :

    Mettre_dans_AH 4Ch

    Interruption_21h

    Ou ainsi (si c'est un COM) :

    Quitter2

    IV-B-1-d. L'inclusion de fichiers

    Il est possible d'accéder à des procédures, des macros ou des définitions EQU qui se trouvent dans d'autres fichiers. Cela permet de se constituer des bibliothèques de macros ou de procédures que l'on peut réutiliser d'un programme à l'autre.

    Pour inclure le fichier TOTO.LIB, écrivez au début de votre code source :

    Image non disponible

    La condition “if1” indique au compilateur que l'inclusion ne doit s'effectuer que lors de la première passe.

    Le fichier TOTO.LIB est un fichier texte tout à fait banal qui contient des lignes de code en assembleur.

    IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE

    Remarques préliminaires :

    • Le principe du langage assembleur est de remplacer chaque opcode hexadécimal par un mot facile à retenir. Ce mot est appelé mnémonique. Par exemple, “INT” est le mnémonique associé à l'opcode CDh. Chaque fois que le compilateur rencontrera ce mot, il le remplacera par l'octet CDh et écrira ensuite l'opérande (ici : le numéro de l'interruption) en hexadécimal.
    • Cette liste récapitule les instructions que nous connaissons déjà et en présente de nouvelles. Elle n'est pas exhaustive, mais vous sera amplement suffisante pour la plupart de vos programmes.
    • Certaines instructions, comme PUSHA, ne sont disponibles que pour des modèles de processeurs plus évolués que le 8086, par exemple le 286. N'oubliez pas la directive .386 si vous les utilisez.

    IV-C-1. L'instruction NOP (« No Operation »)

    Syntaxe : NOP

    Description : Ne fait rien ! Mais alors RIEN ! Que dalle ! Niet !

    IV-C-2. L'instruction MOV (« Move »)

    Syntaxe : MOV Destination, Source

    Description : Copie le contenu de Source dans Destination.

    Mouvements autorisés :

    MOV Registre général, Registre quelconque

    MOV Mémoire, Registre quelconque

    MOV Registre général, Mémoire

    MOV Registre général, Constante

    MOV Mémoire, Constante

    MOV Registre de segment, Registre général

    Remarques : Source et Destination doivent avoir la même taille. On ne peut charger dans un registre de segment que le contenu d'un registre général (SI, DI et BP sont considérés ici comme des registres généraux).

    Exemples :

    MOV AX, 5

    MOV ES, DX

    MOV AL, [Variable1] ; Copie un octet, car AL contient 8 bits

    MOV [Variable2], DS ; Copie un word car DS contient 16 bits

    MOV word ptr [Variable3], 12 ; Ici, on spécifie que la variable est un word

    IV-C-3. L'instruction XCHG (« Exchange »)

    Syntaxe : XCHG Destination, Source

    Description : Échange les contenus de Source et de Destination.

    Mouvements autorisés :

    XCHG Registre général, Registre général

    XCHG Registre général, Mémoire

    XCHG Mémoire, Registre général

    IV-C-4. L'instruction JMP (« Jump »)

    Syntaxe : JMP MonLabel

    Description : Saute à l'instruction pointée par MonLabel.

    IV-C-5. L'opérateur CMP (« Compare »)

    Syntaxe : CMP Destination, Source

    Description : Cet opérateur sert à comparer deux nombres : Source et Destination. C'est le registre des indicateurs qui contient les résultats de la comparaison. Ni Source ni Destination ne sont modifiés.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    Remarque : Cet opérateur effectue en fait une soustraction, mais contrairement à SUB, le résultat n'est pas sauvegardé.

    Le programme doit pouvoir réagir en fonction des résultats de la comparaison. Pour cela, on utilise les sauts conditionnels (voir ci-dessous).

    IV-C-6. Les instructions de saut conditionnel

    Les sauts conditionnels sont terriblement importants, car ils permettent au programme de faire des choix en fonction des données.

    Un saut conditionnel n'est effectué qu'à certaines conditions portant sur les flags (par exemple : CF = 1 ou ZF = 0).

    Certains mnémoniques de sauts conditionnels sont totalement équivalents, c'est-à-dire qu'ils représentent le même opcode hexadécimal. C'est pour aider le programmeur qu'ils existent parfois sous plusieurs formes.

    IV-C-6-a. les sauts de comparaison

    • JE (« Jump if Equal ») fait un saut au label spécifié si et seulement si ZF = 1. Rappelez-vous que ce flag est à 1 si et seulement si le résultat de l'opération précédente vaut zéro. Comme CMP réalise une soustraction, on utilise généralement JE pour savoir si deux nombres sont égaux.

    Exemple :

    Image non disponible

    Mnémonique équivalent : JZ (« Jump if Zero »)

    • JG (« Jump if Greater ») fait un saut au label spécifié si et seulement si ZF = 0 et SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur à un autre.

    Exemple:

    Image non disponible

    Mnémonique équivalent : JNLE (« Jump if Not Less Or Equal »)

    • JGE (« Jump if Greater or Equal ») fait un saut au label spécifié si et seulement si SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur ou égal à un autre.

    Mnémonique équivalent : JNL (« Jump if Not Less »)

    • JL (« Jump if Less ») fait un saut au label spécifié si et seulement si SF <> OF. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur à un autre.

    Mnémonique équivalent : JNGE (« Jump if Not Greater Or Equal »)

    • JLE (« Jump if Less Or Equal ») fait un saut au label spécifié si et seulement si SF <> OF ou ZF = 1. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur ou égal à un autre.

    Mnémonique équivalent : JNG (« Jump if Not Greater »)

    • JA (« Jump if Above ») fait un saut au label spécifié si et seulement si ZF = 0 et CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur à un autre.

    Mnémonique équivalent : JNBE (« Jump if Not Below Or Equal »)

    • JAE (« Jump if Above or Equal ») fait un saut au label spécifié si et seulement si CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur ou égal à un autre.

    Mnémonique équivalent : JNB (« Jump if Not Below »)

    • JB (« Jump if Below ») fait un saut au label spécifié si et seulement si CF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur à un autre.

    Mnémonique équivalent : JNAE (« Jump if Not Above Or Equal »)

    • JBE (« Jump if Below or Equal ») fait un saut au label spécifié si et seulement si CF = 1 ou ZF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur ou égal à un autre.

    Mnémonique équivalent : JNA (« Jump if Not Above »)

    IV-C-6-a-i. les sauts de test sur les flags

    Ces instructions testent un flag unique et exécutent ou non le saut selon la valeur de ce flag.

    • JC (« Jump if Carry ») fait un saut au label spécifié si et seulement si CF = 1.

    Remarques : Ce mnémonique correspond au même opcode que JB. Il est souvent employé pour vérifier que l'appel d'une interruption n'a pas déclenché d'erreur.

    Exemple :

    Image non disponible

    Si l'appel de la fonction 3Eh de l'interruption 21H se solde par une erreur, alors la CF vaudra 1 et le saut sera accompli. Dans le cas opposé, l'exécution continuera normalement de manière linéaire.

    • JNC (« Jump if not Carry ») fait un saut au label spécifié si et seulement si CF = 0.
    • JZ (« Jump if Zero ») fait un saut au label spécifié si et seulement si ZF = 1. Ce mnémonique correspond au même opcode que JE.
    • JNZ (« Jump if not Zero ») fait un saut au label spécifié si et seulement si ZF = 0. Ce mnémonique correspond au même opcode que JNE.
    • JS (« Jump if Sign ») fait un saut au label spécifié si et seulement si SF = 1.
    • JNS (« Jump if not Sign ») fait un saut au label spécifié si et seulement si SF = 0.
    • JO (« Jump if Overflow ») fait un saut au label spécifié si et seulement si OF = 1.
    • JNO (« Jump if not Overflow ») fait un saut au label spécifié si et seulement si OF = 0.
    • JP (« Jump if Parity ») fait un saut au label spécifié si et seulement si PF = 1.
    • JNP (« Jump if not Parity ») fait un saut au label spécifié si et seulement si PF = 0.
    IV-C-6-a-ii. le saut de test sur le registre CX

    JCXZ (« Jump if CX = Zero ») fait un saut au label spécifié si et seulement si CX = 0.

    IV-C-6-b. Les instructions arithmétiques

    IV-C-6-b-i. L'instruction INC (« Increment »)

    Syntaxe : INC Destination

    Description : Incrémente Destination .

    Indicateurs affectés : AF, OF, PF, SF, ZF

    Exemple : INC CL

    IV-C-6-b-ii. L'instruction ADD (« Addition »)

    Syntaxe : ADD Destination , Source

    Description : Ajoute Source à Destination.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    Exemple : ADD byte ptr [ VARIABLE + DI], 5

    IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)

    Syntaxe : ADC Destination , Source

    Description : Ajoute ( Source + CF) à Destination .

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-b-iv. L'instruction DEC (« Decrement »)

    Syntaxe : DEC Destination

    Description : Décrémente Destination .

    Indicateurs affectés : AF, OF, PF, SF, ZF

    IV-C-6-b-v. L'instruction SUB (« Substract »)

    Syntaxe : SUB Destination , Source

    Description : Soustrait Source à Destination .

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)

    Syntaxe : SBB Destination , Source

    Description : Soustrait ( Source + CF) à Destination .

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-b-vii. L'instruction MUL (« Multiply »)

    Syntaxe : MUL Source

    Description : Effectue une multiplication d'entiers non signés .

    • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
    • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
    • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

    Indicateurs affectés : CF, OF

    Remarque : Source ne peut être une valeur immédiate.

    Exemples : MUL CX

    MUL byte ptr [ TOTO ]

    IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)

    Syntaxe : IMUL Source

    IMUL Destination , Source

    IMUL Destination , Source , Valeur

    Description : Effectue une multiplication d'entiers signés .

    IMUL Source :

    • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
    • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
    • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

    IMUL Destination , Source : Multiplie Destination par Source et place le résultat dans Destination .

    IMUL Destination , Source , Valeur : Multiplie Source par Valeur et place le résultat dans Destination .

    Indicateurs affectés : CF, OF

    IV-C-6-b-ix. L'instruction DIV (« Divide »)

    Syntaxe : DIV Source

    Description : Effectue une division euclidienne d'entiers non signés .

    • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
    • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
    • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

    Indicateurs affectés : AF, OF, PF, SF, ZF

    Remarque : Source ne peut être une valeur immédiate.

    IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)

    Syntaxe : IDIV Source

    Description : Effectue une division euclidienne d'entiers signés .

    • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
    • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
    • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-b-xi. L'instruction NEG (« Negation »)

    Syntaxe : NEG Destination

    Description : Forme le complément à 2 de Destination , i.e. prend l'opposé de Destination .

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    Remarque : À ne pas confondre avec NOT, qui forme le complément à 1.

    IV-C-6-c. Les instructions logiques

    IV-C-6-c-i. L'instruction NOT (« Logical NOT »)

    Syntaxe : NOT Destination

    Description : Effectue un NON logique bit à bit sur Destination (i.e. chaque bit de Destination est inversé).

    IV-C-6-c-ii. L'instruction OR (« Logical OR »)

    Syntaxe : OR Destination , Source

    Description : Effectue un OU logique inclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

    Indicateurs affectés : CF, OF, PF, SF, ZF

    Remarque : Afin d'optimiser la taille et les performances du programme, on peut utiliser l'instruction “OR AX, AX” à la place de “CMP AX, 0”.
    En effet, un OU bit à bit entre deux nombres identiques ne modifie pas Destination et est exécuté « infiniment » plus rapidement qu'une soustraction. Comme les flags sont affectés, les sauts conditionnels sont possibles.

    IV-C-6-c-iii. L'instruction AND (« Logical AND »)

    Syntaxe : AND Destination , Source

    Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

    Indicateurs affectés : CF, OF, PF, SF, ZF

    IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)

    Syntaxe : TEST Destination , Source

    Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat n'est pas conservé, donc Destination n'est pas modifié. Seuls les flags sont affectés.

    Cet opérateur est souvent utilisé pour tester certains bits de Destination .

    Indicateurs affectés : CF, OF, PF, SF, ZF

    Exemple :

    IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)

    Syntaxe : XOR Destination , Source

    Description : Effectue un OU logique exclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

    Indicateurs affectés : CF, OF, PF, SF, ZF

    Remarque : Pour remettre un registre à zéro, il est préférable de faire “XOR AX, AX” que “MOV AX, 0”. En effet, le résultat est le même, mais la taille et surtout la vitesse d'exécution de l'instruction sont très largement optimisées.

    IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)

    Syntaxe : SHL Destination , Source

    Description : Décale les bits de Destination de Source positions vers la gauche. Les bits les plus à droite sont remplacés par des zéros.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    Exemple : SHL AX, 4 ;permet de multiplier par 16 de façon infiniment plus rapide que MUL

    Mnémonique équivalent : SAL (« Shift Arithmetical Left »)

    IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)

    Syntaxe : SHR Destination , Source

    Description : Décale les bits de Destination de Source positions vers la droite. Les bits les plus à gauche sont remplacés par des zéros.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    Mnémonique équivalent : SAR (« Shift Arithmetical Right »)

    IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)

    Syntaxe : ROL Destination , Source

    Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. Le dernier bit à être sorti à gauche et à être rentré à droite est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)

    Syntaxe : ROR Destination , Source

    Description : Effectue une rotation des bits de Destination de Source positions vers la droite. Le dernier bit à être sorti à droite et à être rentré à gauche est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)

    Syntaxe : RCL Destination , Source

    Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. CF est utilisé comme intermédiaire : chaque bit qui sort à gauche est placé dans CF, et le contenu de CF est ensuite réinséré à droite. OF est mis à 1 si et seulement si le signe de Destination a changé.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)

    Syntaxe : RCR Destination , Source

    Description : Effectue une rotation des bits de Destination de Source positions vers la droite. CF est utilisé comme intermédiaire : chaque bit qui sort à droite est placé dans CF, et le contenu de CF est ensuite réinséré à gauche. OF est mis à 1 si et seulement si le signe de Destination a changé.

    Indicateurs affectés : AF, CF, OF, PF, SF, ZF

    IV-C-6-d. Les instructions de manipulation des flags

    IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)

    Syntaxe : CLC

    Description : Met CF à 0.

    Indicateurs affectés : CF

    IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)

    Syntaxe : STC

    Description : Met CF à 1.

    Indicateurs affectés : CF

    IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)

    Syntaxe : CLD

    Description : Met DF à 0.

    Indicateurs affectés : DF

    IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)

    Syntaxe : STD

    Description : Met DF à 1.

    Indicateurs affectés : DF

    IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)

    Syntaxe : CLI

    Description : Met IF à 0.

    Indicateurs affectés : IF

    IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)

    Syntaxe : STI

    Description : Met IF à 1.

    Indicateurs affectés : IF

    IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)

    Syntaxe : CMC

    Description : Inverse CF.

    Indicateurs affectés : CF

    IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)

    Syntaxe : LAHF

    Description : Charge dans AH l'octet de poids faible du registre des indicateurs.

    IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)

    Syntaxe : SAHF

    Description : Stocke les bits de AH dans le registre des indicateurs.

    IV-C-6-e. Les instructions de gestion de la pile

    IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)

    Syntaxe : PUSH Source

    Description : Empile le mot Source. SP est décrémenté de 2.

    Remarques : Source ne peut être une valeur immédiate. Il est possible d'abréger votre code source en écrivant par exemple “PUSH AX BX BP”. Le compilateur écrira alors trois fois l'instruction PUSH du langage machine. Il est possible également d'empiler des doubles-mots.

    IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)

    Syntaxe : POP Destination

    Description : Dépile le mot qui se trouve au sommet de la pile et le place dans Destination. SP est incrémenté de 2.

    IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)

    Syntaxe : PUSHF

    Description : Empile le registre des indicateurs. SP est décrémenté de 2.

    Remarque : PUSHFD empile le registre des indicateurs codé sur 32 bits.

    IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)

    Syntaxe : POPF

    Description : Dépile le mot qui se trouve au sommet de la pile et le place dans le registre des indicateurs. SP est incrémenté de 2.

    Indicateurs affectés : Tous

    Remarque : POPFD est utilisé pour un registre des indicateurs codé sur 32 bits.

    IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)

    Syntaxe : PUSHA

    Description : Empile AX, BX, CX, DX, BP, SI, DI et SP.

    Remarque : PUSHAD est utilisé pour des registres de 32 bits.

    IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)

    Syntaxe : POPA

    Description : Restaure AX, BX, CX, DX, BP, SI, DI et SP à partir de la pile.

    Remarque : POPAD est utilisé pour des registres de 32 bits.

    IV-C-6-f. Les instructions de gestion des chaînes d'octets

    IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)

    Syntaxe : MOVSB

    Description : Copie l'octet adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés, sinon ils sont décrémentés.

    Remarque : Pour copier plusieurs octets, faire REP MOVSB (« Repeat Move String Byte »). Le nombre d'octets à copier doit être transmis dans CX de même que pour un LOOP.

    Exemple :

    Image non disponible

    IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)

    Syntaxe : SCASB

    Description : Compare l'octet adressé par ES:DI avec AL. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

    Remarques : Pour comparer plusieurs octets, faire “REP SCASB” ou “REPE SCASB” (« Repeat until Egal »), ou encore “REPZ SCASB” (« Repeat until Zero »). Ces trois préfixes sont équivalents.
    Le nombre d'octets à comparer doit être transmis dans CX. La boucle ainsi créée s'arrête si CX = 0 ou si le caractère pointé par ES:DI est le même que celui contenu dans AL (i.e. si ZF = 1). On peut ainsi rechercher un caractère dans une chaîne.
    Pour répéter au contraire la comparaison jusqu'à ce que ZF = 0, c'est-à-dire jusqu'à ce que AL et le caractère adressé par ES:DI diffèrent, utiliser REPNE ou REPNZ.

    Exemple :

    Image non disponible

    IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)

    Syntaxe : LODSB

    Description : Charge dans AL l'octet adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté, sinon il est décrémenté.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour MOVSB.

    Syntaxe : STOSB

    Description : Stocke le contenu de AL dans l'octet adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

    Syntaxe : CMPSB

    Description : Compare l'octet adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés, sinon ils sont décrémentés.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

    IV-C-6-g. Les instructions de gestion des chaînes de mots

    Ce sont les mêmes que les précédentes, hormis qu'elles traitent des mots et non des octets et que leur nom se termine par un ‘W' au lieu du ‘B'.

    IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)

    Syntaxe : MOVSW

    Description : Copie le mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

    Remarque : Pour copier plusieurs mots, faire “REP MOVSW”. Le nombre de mots à copier doit être transmis dans CX.

    IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)

    Syntaxe : SCASW

    Description : Compare le mot adressé par ES:DI avec AX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

    IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)

    Syntaxe : LODSW

    Description : Charge dans AX le mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

    IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)

    Syntaxe : STOSW

    Description : Stocke le contenu de AX dans le mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

    IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)

    Syntaxe : CMPSW

    Description : Compare le mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

    IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots

    Elles traitent des doubles-mots et leur nom se termine par un ‘D'.

    IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)

    Syntaxe : MOVSD

    Description : Copie le double-mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

    Remarque : Pour copier plusieurs doubles-mots, faire “REP MOVSD”. Le nombre de doubles-mots à copier doit être transmis dans CX.

    IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)

    Syntaxe : SCASD

    Description : Compare le double-mot adressé par ES:DI avec EAX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

    IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)

    Syntaxe : LODSD

    Description : Charge dans EAX le double-mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

    IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)

    Syntaxe : STOSD

    Description : Stocke le contenu de EAX dans le double-mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

    IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »

    Syntaxe : CMPSD

    Description : Compare le double-mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

    Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

    IV-C-6-i. Les instructions d'appel

    IV-C-6-i-i. L'instruction CALL (« Procedure Call »)

    Syntaxe : CALL MaProc

    Description : Appel de procédure. Si MaProc se trouve dans un segment extérieur, le processeur empile CS. Ensuite, dans tous les cas, il empile IP et fait un saut à l'étiquette MaProc.

    IV-C-6-i-ii. L'instruction RET (ou RETN)

    Syntaxe : RET

    Description : Retour de procédure se trouvant à l'intérieur du segment (NEAR). Un mot est dépilé et placé dans IP. Le contrôle retourne donc à la procédure appelante.

    IV-C-6-i-iii. L'instruction RETF

    Syntaxe : RETF

    Description : Retour de procédure se trouvant à l'extérieur du segment (FAR). Deux mots sont dépilés et placés dans CS:IP. Le contrôle retourne donc à la procédure appelante.

    IV-C-6-j. Les instructions de boucle

    IV-C-6-j-i. L'instruction LOOP

    Syntaxe : LOOP MonLabel

    Description : Décrémente CX, puis, si CX <> 0, fait un saut à MonLabel.

    IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)

    Syntaxe : LOOPE MonLabel

    Description : Décrémente CX, puis, si CX <> 0 et ZF = 1, fait un saut à MonLabel.

    Mnémonique équivalent : LOOPZ

    IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)

    Syntaxe : LOOPNE MonLabel

    Description : Décrémente CX, puis, si CX <> 0 et ZF = 0, fait un saut à MonLabel.

    Mnémonique équivalent : LOOPNZ

    IV-C-6-k. Les instructions d'adressage

    IV-C-6-k-i. L'instruction LEA (« Load effective address »)

    Syntaxe : LEA Destination, Source

    Description : Charge l'offset de la source dans le registre Destination.

    Exemple : LEA BP, word ptr [BP + TOTO]

    IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)

    Syntaxe : LDS Destination, Source

    Description : Transfère dans DS:Destination le contenu de la mémoire adressée par Source.

    IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)

    Syntaxe : LES Destination, Source

    Description : Transfère dans ES:Destination le contenu de la mémoire adressée par Source.

    IV-C-6-l. Les instructions de conversion arithmétique

    Remarque préliminaire : Nous n'expliciterons pas toutes ces instructions.

    IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
    IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
    IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
    IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
    IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)

    Syntaxe : CBW

    Description : Convertit l'octet signé stocké dans AL en un mot (signé) stocké dans AX. Ainsi, si AL est négatif, AH sera rempli de 1 binaires, sinon, AH sera mis à 0.

    IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)

    Syntaxe : CWD

    Description : Convertit le mot signé stocké dans AX en un double-mot (signé) stocké dans DX:AX. Ainsi, si AX est négatif, DX sera rempli de 1 binaires, sinon DX sera mis à 0.

    IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
    IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
    IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)

    Syntaxe : MOVSX Destination, Source

    Description : Déplace le contenu signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. Si Source est négatif, la partie haute de Destination sera remplie de 1 binaires, sinon elle sera remplie de 0.

    IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)

    Syntaxe : MOVZX Destination, Source

    Description : Déplace le contenu non signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. La partie haute de Destination sera donc mise à 0.

    IV-C-6-l-xi. L'instruction XLAT (« Translate »)

    IV-C-6-m. Les instructions d'entrée-sortie

    IV-C-6-m-i. L'instruction IN (« Input from port »)

    Syntaxe : IN Destination, Port

    Description : Charge un octet ou un mot depuis un port d'entrée-sortie dans AL ou AX. Port peut être DX ou bien une constante de 8 bits.

    IV-C-6-m-ii. L'instruction OUT (« Output to port »)

    Syntaxe : OUT Port, Source

    Description : Écrit dans Port la valeur contenue dans Source. Source ne peut être que AL ou AX. Port est une constante ou bien DX.

    V. QUATRIÈME PARTIE

    Remarque préliminaire : Cette partie présente quelques-unes des interruptions du DOS qui servent à manier les fichiers d'un disque. Nous la conclurons par l'étude d'un exemple de programme qui crypte un fichier choisi par l'utilisateur.

    V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES

    Sous DOS, il existe deux méthodes pour accéder aux fichiers et pour chacune d'elles un lot d'interruptions spécifiques. La première est la méthode des FCB (« File contol block »). Elle est rarement utilisée, aussi ne l'aborderons-nous pas. Nous étudierons le principe de la méthode des « handles ».

    Pour lire ou écrire des données dans un fichier, il est nécessaire de l'ouvrir, c'est-à-dire de le charger en mémoire. Quand toutes les opérations de lecture et d'écriture auront été effectuées, le fichier devra être refermé afin d'enregistrer les éventuelles dernières modifications et surtout de libérer la mémoire occupée.

    V-A-1. Ouverture d'un fichier

    On ouvre un fichier en appelant la fonction 3dh de l'interruption 21h. Celle-ci attend comme paramètre dans DS:DX l'adresse d'une chaîne de caractères qui contient le chemin d'accès au fichier sur un disque, par exemple “C:\MonDoss\MonFic.txt”.

    Remarque : il n'est pas indispensable de mentionner le chemin d'accès complet : par défaut, le fichier sera cherché à partir du dossier courant.

    Remarque importante : La chaîne doit être impérativement suivie de l'octet 00h qui sert à marquer sa fin.

    Il nous faut également spécifier le mode d'accès en écrivant dans AL un 0 (si on veut ouvrir le fichier en lecture seule), un 1 (si on veut l'ouvrir en écriture seule) ou un 2 (lecture ET écriture).

    Si l'interruption échoue, le flag CF sera mis à 1 sans que le fichier soit ouvert. Dans le cas contraire, CF est mis 0 et le registre AX contient un petit nombre entier (par exemple 5) appelé « handle » (ce qui signifie « poignée ») du fichier. Ce handle représente le fichier. C'est lui qu'il faudra désormais invoquer pour effectuer des opérations de lecture ou d'écriture, et non pas le chemin d'accès.
    En effet, les chemins d'accès ne sont donc plus d'aucune utilité puisque les fichiers sont ouverts dans la mémoire vive.

    V-A-2. Lecture dans un fichier

    Une fois le fichier ouvert, on peut le lire avec la fonction 3fh. Il suffit de mentionner le handle dans BX, le nombre d'octets à lire dans CX, et l'adresse d'un buffer dans DS:DX.
    Au cas où vous ne sauriez pas ce qu'est un buffer (ou tampon), sachez que c'est simplement une variable (généralement une chaîne de caractères) destinée à recevoir des données (ou à en fournir). Dans notre cas, le buffer va recevoir les octets lus dans le fichier.

    Après l'appel, AX contient le nombre d'octets qui ont été effectivement lus (il peut être inférieur à la taille demandée si le fichier n'est pas assez long). En cas de problème, CF sera mis à 1.

    V-A-3. Écriture dans un fichier

    Pour écrire des données, on procède de même avec la fonction 40h. Les paramètres sont les mêmes que pour la fonction 3fh. Le buffer contient cette fois les octets à écrire. Après l'appel, le nombre d'octets qui ont été effectivement écrits est stocké dans AX (il sera être inférieur à la taille spécifiée si le disque est plein).

    Les données sont écrites sur le disque dur que le tampon (dans la mémoire vive) est plein.

    V-A-4. Existence d'un pointeur de fichier

    Une question se pose cependant : à quel endroit du fichier les données sont-elles lues (ou écrites) ?

    Réponse : quand un fichier est ouvert, un pointeur spécial pointant vers le début du fichier est créé. La première opération de lecture (ou d'écriture) se fera donc au début du fichier.
    Mais entre chaque opération, le pointeur est incrémenté de la taille des données que l'on a lues (ou écrites). La deuxième opération se fera donc sur les octets qui suivent ceux de la première.

    Remarque : Il est possible de modifier directement le pointeur de fichier : voyez pour cela la fonction 42h…

    V-A-5. Fermeture d'un fichier

    Pour terminer, le fichier doit être refermé. Les modifications éventuellement apportées et non enregistrées seront écrites sur le disque, et le handle sera libéré. C'est la fonction 3eh qui se charge de tout cela. Elle attend simplement le handle du fichier dans BX. Et comme d'habitude, CF vaut 1 après l'appel si des erreurs ont été rencontrées.

    Remarque : Attention lorsque vous laissez le fichier ouvert longtemps afin d'y ajouter progressivement des données ! Si le système plante, vous perdrez les données qui se trouvent dans le tampon à ce moment. C'est pourquoi il est conseillé de forcer régulièrement l'écriture sur le disque en refermant le fichier.

    V-A-6. Conclusion

    Le tableau suivant récapitule ces différentes étapes :

    Fonction | Description | Paramètres
3d h | Ouvrir le fichier | - DS:DX : adresse d'une chaîne contenant le chemin d'accès - AL : mode d'accès
3e h | Fermer le fichier | - BX : handle
3f h | Lire le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer
40 h | Écrire dans le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer

    V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS▲

    Pour rechercher un fichier (ou un dossier), on se sert des fonctions 4eh (« Find First ») et 4fh (« Find Next »).

    Imaginons par exemple que nous voulions chercher dans le dossier courant tous les fichiers qui portent l'extension “ .com ” afin de les supprimer. Comment devons-nous nous y prendre ?

    V-B-1. La fonction 4eh

    La fonction 4eh sert à définir des critères de recherche et à trouver le premier fichier qui correspond à ces critères (s'il existe).

    On doit passer dans DS:DX l'adresse de la chaîne de caractères qui contient le masque de recherche (dans notre exemple, ce masque est “ *.com ”). Par défaut, les fichiers sont cherchés dans le dossier courant. Mais on peut évidemment spécifier un autre chemin dans le masque.

    Remarque importante : afin que le DOS puisse connaître sa taille, le masque doit impérativement être terminé par l'octet 00h !

    On écrit également dans CX les attributs des fichiers que l'on désire trouver. Si CX vaut 0, seuls les fichiers « normaux » pourront être trouvés. En fait, chaque bit de CL représente un attribut, comme le montre le tableau ci-dessous :

    Bit | Signification
1 | Lecture seule
2 | Fichier caché
3 | Fichier système
4 | Volume
5 | Répertoire
6 | Fichier
7 | (Aucune…)
8 | (Aucune…)

    Pour demander à la fonction 4eh de ne pas oublier les fichiers cachés, il suffit donc de charger CX avec la valeur 2 (bit numéro 2 = 1). De même, l'attribut 00000111 b (soit 7) nous permettra de trouver les fichiers en lecture seule, les fichiers cachés et les fichiers systèmes.

    Remarque : Ne vous souciez pas trop des bits numéro 4 et 6. Laissez-les à 0.

    Une fois que les paramètres ont été ajustés, on peut appeler la fonction 4eh. Si aucun fichier n'a été trouvé, le flag CF est mis à 1 . On doit donc faire un test sur CF pour savoir si la recherche peut continuer ou si elle doit s'arrêter.
    Si au contraire la fonction a trouvé un fichier, les caractéristiques de ce fichier (i.e. son nom, sa taille, ses attributs    …) sont inscrites dans une zone de la mémoire appelée DTA (« Disk Transfer Area »).

    Mais où se trouve donc cette DTA et à quoi ressemble-t-elle ?

    Réponse : par défaut, le DOS place la DTA dans le PSP de votre programme, à l'offset 80h.

    Remarque : il vous est naturellement possible de la déplacer en faisant appel à la fonction 1ah.

    Voici la structure de la DTA :

    Adresse | Description | Taille (octets)
00 h | Lettre du lecteur (0=courant, 1=A, 2=B…) sur lequel se trouve le fichier | 1
01 h | Modèle de la recherche | 11
0C h | Réservé | 9
15 h | Attributs du fichier | 1
16 h | Heure du fichier | 2
18 h | Date du fichier | 2
1A h | Taille du fichier | 4
1E h | Nom du fichier avec l'extension | 13

    Puisque par défaut la DTA est située dans le PSP à l'offset 80h, le nom du fichier trouvé est écrit à l'offset 80h + 1eh = 9eh. De même, la taille se trouve à l'offset 9ah, etc.

    V-B-2. La fonction 4fh

    Jusqu'à présent, nous n'avons trouvé qu'un seul fichier ! Pour poursuivre la recherche, appelez la fonction 4fh sans écrire aucun paramètre dans les registres. Les caractéristiques de votre recherche ont été mémorisées dans la DTA : vous n'avez donc pas à les rappeler. De même que pour la fonction 4eh, CF est mis à 1 si aucun nouveau fichier n'a été trouvé. C'est le signe que vous pouvez arrêter votre recherche.

    V-B-3. Conclusion

    Résumons-nous :

    Fonction | Description | Paramètres
4e h | Trouver le premier fichier qui correspond aux caractéristiques spécifiées | - DS:DX : adresse d'une chaîne contenant le masque - CX : attributs
4f h | Trouver le prochain fichier | Aucun !

    À titre d'exemple, écrivons à présent le programme que nous évoquions tout à l'heure : “ trouver tous les fichiers COM du dossier courant et les effacer ”.

    V-B-4. EXEMPLE DE PROGRAMME

    V-B-5. Entrée bufferisée au clavier

    L'exemple que nous proposons ici fait appel à une saisie au clavier. Comme nous n'en avons pas encore rencontré, ce paragraphe explique brièvement comment utiliser les fonctions 0ah et 0ch.

    On peut se servir de la fonction 0ah pour lire une chaîne de caractères au clavier. Le seul paramètre à fournir est l'adresse d'un buffer. Comme il est souvent indispensable d'effacer le buffer avant la lecture, il est préférable de ne pas utiliser la fonction 0ah directement, mais d'appeler la fonction 0ch. En effet, celle-ci commence par effacer le buffer, puis appelle la fonction 0ah (ou une fonction voisine dont le numéro doit être transmis dans AL).

    Voici le code à écrire :

    Image non disponible

    La seule difficulté est dans la déclaration du buffer. Voici comment vous devez vous y prendre si vous considérez que l'utilisateur pourra entrer au plus n caractères :

    Image non disponible

    Le premier octet de “maChaine” doit contenir le nombre d'octets maximal qui pourront être entrés, plus 1. Pourquoi plus 1 ? Tout simplement parce que le DOS met à la fin des caractères tapés le code ASCII 13 (retour chariot).

    Pour comprendre la suite de la déclaration, il faut savoir de quelle manière le DOS transmet les caractères qui ont été lus. Après la lecture, le deuxième octet du buffer contiendra le nombre exact d'octets lus (sans compter le 13 final). La chaîne proprement dite ne commencera donc qu'au troisième octet. Puisqu'un octet est utilisé pour donner au DOS le nombre maximal de caractères autorisés, un autre pour recevoir le nombre de caractères lus effectivement, et encore un autre pour recevoir le code ASCII 13, le buffer doit comporter trois octets de plus que la taille maximale de la chaîne. C'est pourquoi on écrit :

    maChaine db n+1, ?, n dup(?), ?

    V-B-6. Le programme

    Le programme suivant demande à l'utilisateur d'entrer le nom d'un fichier se trouvant dans le dossier courant puis crypte ce fichier en appliquant un NOT logique sur chaque octet. Naturellement, il n'est pas optimisé du tout. Ce n'est qu'un exemple !

    Image non disponible

    Suggestion : quand vous aurez compris ce petit programme, essayez donc de l'améliorer ! Par exemple, il faudrait que l'utilisateur puisse taper le nom du fichier à crypter directement comme paramètre du programme (i.e. “CRY MONFICH.EXE”). Voyez pour cela la structure du PSP telle que présentée dans la première partie. De plus, le programme devrait pouvoir trouver automatiquement le nom du fichier d'arrivée : ce serait le même que celui du fichier à crypter, mais avec une extension différente (par exemple “.cry”).
    Vous pourriez également étendre les possibilités du logiciel en permettant à l'utilisateur de saisir non plus un simple nom de fichier, mais un masque complexe avec des chemins d'accès et des caractères     “jokers” comme '*' et '?'. Vous aurez donc besoin des fonctions de recherche 4eh et 4fh. L'utilisateur doit de surcroît pouvoir choisir s'il veut crypter un fichier ou bien le décrypter…

    VI. CONCLUSION

    Voilà. Nous espérons que vous avez compris l'essentiel de ce cours, à savoir la logique de la programmation en assembleur. Les connaissances que vous avez acquises en lisant la fin du cours devraient vous permettre de mieux saisir les premiers chapitres. C'est pourquoi nous vous invitons à relire ce tutoriel.

    Certaines notions doivent encore vous paraître obscures. Maintenant que vous vous êtes familiarisé avec l'assembleur, commencez par écrire et tester quelques petits programmes COM inutiles et très simples. Vous verrez qu'au bout de quelques essais, tout ce que vous avez lu vous semblera très concret.

    Lorsque vous aurez écrit vous-même quelques tout petits programmes qui fonctionnent, l'assimilation du langage deviendra alors très rapide, car l'assembleur est un langage très logique et très cohérent.

    Nous vous conseillons d'écrire une bibliothèque de petites macros ou procédures qui vous serviront dans tous vos programmes. Par exemple, vous pouvez faire une macro qui affiche un entier à l'écran, ou bien qui convertit une chaîne de caractères en un nombre entier.

    Voici un exemple de macro qui renvoie dans AL le nombre de chiffres d'un entier non signé de deux octets passé dans AX :

    Image non disponible

    N.B. Pensez avant toute chose à vous munir d'une liste des interruptions du DOS. Vous ne pourrez rien faire si vous n'en avez pas !

    Bon courage et bonne chance !

    Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Benoît-M. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.


ASM Mode Reel

Langage Assembleur PC

Table des matières Plier Déplier

  • I. Préface
    • I-A. Objectif
    • I-B. Remerciements
    • I-C. Ressources sur Internet
    • I-D. Réactions
  • II. Introduction
    • II-A. Systèmes numériques
      • II-A-1. Décimal
      • II-A-2. Binaire
      • II-A-3. Hexadécimal
    • II-B. Organisation de l'ordinateur
      • II-B-1. Mémoire
      • II-B-2. Le CPU (processeur)
      • II-B-3. La famille des processeurs 80x86
      • II-B-4. Regitres 16 bits du 8086
      • II-B-5. Registres 32 bits du 80386
      • II-B-6. Mode réel
      • II-B-7. Mode protégé 16 bits
      • II-B-8. Mode protégé 32 bits
      • II-B-9. Interruptions
    • II-C. Langage assembleur
      • II-C-1. Langage machine
      • II-C-2. Langage d'assembleur
      • II-C-3. Opérandes d'instruction
      • II-C-4. Instructions de base
      • II-C-5. Directives
        • II-C-5-a. La directive equ
        • II-C-5-b. La directive ?ne
        • II-C-5-c. Directives de données
      • II-C-6. Entrées et sorties
      • II-C-7. Débogage
    • II-D. Créer un programme
      • II-D-1. Premier programme
      • II-D-2. Dépendance vis-à-vis du compilateur
      • II-D-3. Assembler le code
      • II-D-4. Compiler le code C
      • II-D-5. Lier les fichiers objet
      • II-D-6. Comprendre un listing assembleur
    • II-E. Fichier squelette
  • III. Bases du langage assembleur
    • III-A. Travailler avec les entiers
      • III-A-1. Représentation des entiers
        • III-A-1-a. Grandeur signée
        • III-A-1-b. Complément à 1
        • III-A-1-c. Complément à 2
      • III-A-2. Extension de signe
        • III-A-2-a. Réduire la taille des données
        • III-A-2-b. Augmenter la taille des données
        • III-A-2-c. Application à la programmation en C
      • III-A-3. Arithmétique en complément à 2
      • III-A-4. Programme exemple
      • III-A-5. Arithmétique en précision étendue
    • III-B. Structures de contrôle
      • III-B-1. Comparaison
      • III-B-2. Instructions de branchement
      • III-B-3. Les instructions de boucle
    • III-C. Traduire les structures de contrôle standard
      • III-C-1. Instructions if
      • III-C-2. Boucles while
      • III-C-3. Boucles do while
    • III-D. Exemple : trouver des nombres premiers
  • IV. Opérations sur les bits
    • IV-A. Opérations de décalage
      • IV-A-1. Décalages logiques
      • IV-A-2. Utilisation des décalages
      • IV-A-3. Décalages arithmétiques
      • IV-A-4. Décalages circulaires
      • IV-A-5. Application simple
    • IV-B. Opérations booléennes niveau bit
      • IV-B-1. L'opération ET
      • IV-B-2. L'opération OU
      • IV-B-3. L'opération XOR
      • IV-B-4. L'opération NOT
      • IV-B-5. L'instruction TEST
      • IV-B-6. Utilisation des opérations sur les bits
    • IV-C. Éviter les branchements conditionnels
    • IV-D. Manipuler les bits en C
      • IV-D-1. Les opérateurs niveau bit du C
      • IV-D-2. Utiliser les opérateurs niveau bit en C
    • IV-E. Représentations big et little endian
      • IV-E-1. Quand se soucier du caractère big ou little endian
    • IV-F. Compter les bits
      • IV-F-1. Méthode une
      • IV-F-2. Méthode deux
      • IV-F-3. Méthode trois
  • V. Sous-programmes
    • V-A. Adressage indirect
    • V-B. Exemple de sous-programme simple
    • V-C. La pile
    • V-D. Les Instructions CALL et RET
    • V-E. Conventions d'appel
      • V-E-1. Passer les paramètres via la pile
      • V-E-2. Variables locales sur la pile
    • V-F. Programme multimodule
    • V-G. Interfacer de l'assembleur avec du C
      • V-G-1. Sauvegarder les registres
      • V-G-2. Étiquettes de fonctions
      • V-G-3. Passer des paramètres
      • V-G-4. Calculer les adresses des variables locales
      • V-G-5. Retourner des valeurs
      • V-G-6. Autres conventions d'appel
      • V-G-7. Exemples
      • V-G-8. Appeler des fonctions C depuis l'assembleur
    • V-H. Sous-programmes réentrants et récursifs
      • V-H-1. Sous-programmes récursifs
      • V-H-2. Révision des types de stockage des variables en C
  • VI. Tableaux
    • VI-A. Introduction
      • VI-A-1. Définir des tableaux
        • VI-A-1-a. Définir des tableaux dans les segments data et bss
        • VI-A-1-b. Définir des tableaux comme variables locales
      • VI-A-2. Accéder aux éléments de tableaux
      • VI-A-3. Adressage indirect plus avancé
      • VI-A-4. Exemple
        • VI-A-4-a. L'instruction LEA revisitée
      • VI-A-5. Tableaux Multidimensionnels
        • VI-A-5-a. Tableaux à Deux Dimensions
        • VI-A-5-b. Dimensions supérieures à deux
        • VI-A-5-c. Passer des tableaux multidimensionnels comme paramètres en C
    • VI-B. Instructions de tableaux/chaînes
      • VI-B-1. Lire et écrire en mémoire
      • VI-B-2. Le préfixe d'instruction REP
      • VI-B-3. Instructions de comparaison de chaînes
      • VI-B-4. Les préfixes d'instruction REPx
      • VI-B-5. Exemple
  • VII. Virgule flottante
    • VII-A. Représentation en virgule flottante
      • VII-A-1. Nombres binaires non entiers
      • VII-A-2. Représentation en virgule flottante IEEE
        • VII-A-2-a. Simple précision IEEE
        • VII-A-2-b. Nombres dénormalisés
        • VII-A-2-c. Double précision IEEE
    • VII-B. Arithmétique en virgule flottante
      • VII-B-1. Addition
      • VII-B-2. Soustraction
      • VII-B-3. Multiplication et division
      • VII-B-4. Conséquences sur la programmation
    • VII-C. Le coprocesseur arithmétique
      • VII-C-1. Matériel
      • VII-C-2. Instructions
        • VII-C-2-a. Chargement et stockage
        • VII-C-2-b. Addition et soustraction
        • VII-C-2-c. Multiplication et division
        • VII-C-2-d. Comparaisons
        • VII-C-2-e. Instructions diverses
      • VII-C-3. Exemple : formule quadratique
      • VII-C-4. Exemple : lire un tableau depuis un fichier
      • VII-C-5. Exemple : rechercher les nombres premiers
  • VIII. Structures et C++
    • VIII-A. Structures
      • VIII-A-1. Introduction
      • VIII-A-2. Alignement en mémoire
      • VIII-A-3. Champs de bits
      • VIII-A-4. Utiliser des structures en assembleur
    • VIII-B. Assembleur et C++
      • VIII-B-1. Surcharge et décoration de noms
      • VIII-B-2. Références
      • VIII-B-3. Fonctions inline
      • VIII-B-4. Classes
        • VIII-B-4-a. Exemple
      • VIII-B-5. Héritage et polymorphisme
      • VIII-B-6. Autres fonctionnalités C++
  • IX. Annexe A : Instructions 80x86
    • IX-A. Instructions hors virgule flottante
    • IX-B. Instruction en virgule flottante

Copyright © 2001, 2002, 2004 by Paul Carter. Traduction par Sébastien Le Ray .

Ce document peut être reproduit et distribué dans sa totalité (en incluant cette note de l'auteur, le copyright et l'indication de l'autorisation), sous réserve qu'aucuns frais ne soient demandés pour le document lui-même, sans le consentement de l'auteur. Cela inclut une utilisation raisonnable d'extraits pour des revues ou de la publicité et les travaux dérivés comme les traductions.
Notez que ces restrictions ne sont pas destinées à empêcher la participation aux frais pour l'impression ou la copie du document.
Les enseignants sont encouragés à utiliser ce document comme support de cours ; cependant, l'auteur apprécierait d'être averti dans ce cas.

Article lu fois.

Les deux auteurs

Paul A. Carter Site personnel Sébastien Le Ray Site personnel Blog

L'article

Publié le 20 mars 2005 - Mis à jour le 30 janvier 2013

I. Préface

I-A. Objectif

L'objectif de ce livre est de permettre au lecteur de mieux comprendre comment les ordinateurs fonctionnent réellement à un niveau plus bas que les langages de programmation comme Pascal. En ayant une compréhension plus profonde de la façon dont fonctionnent les ordinateurs, le lecteur peu devenir plus productif dans le développement de logiciels dans des langages de plus haut niveau comme le C et le C++.

Apprendre à programmer en assembleur est un excellent moyen d'atteindre ce but. Les autres livres d'assembleur pour PC apprennent toujours à programmer le processeur 8086 qu'utilisait le PC originel de 1981 ! Le processeur 8086 ne supportait que le mode réel.

Dans ce mode, tout programme peu adresser n'importe quel endroit de la mémoire ou n'importe quel périphérique de l'ordinateur. Ce mode n'est pas utilisable pour un système d'exploitation sécurisé et multitâche. Ce livre parle plutôt de la façon de programmer les processeurs 80386 et plus récents en mode protégé (le mode dans lequel fonctionnent Windows et Linux). Ce mode supporte les fonctionnalités que les systèmes d'exploitation modernes offrent, comme la mémoire virtuelle et la protection mémoire. Il y a plusieurs raisons d'utiliser le mode protégé :

1. Il est plus facile de programmer en mode protégé qu'en mode réel 8086 que les autres livres utilisent ;

2. Tous les systèmes d'exploitation PC modernes fonctionnent en mode protégé ;

3. Il y a des logiciels libres disponibles qui fonctionnent dans ce mode.

Le manque de livres sur la programmation en assembleur PC en mode protégé est la principale raison qui a conduit l'auteur à écrire ce livre.

Comme nous y avons fait allusion ci-dessus, ce texte utilise des logiciels Libres/Open Source : l'assembleur NASM et le compilateur C/C++ DJGPP. Les deux sont disponibles en téléchargement sur Internet. Ce texte parle également de l'utilisation de code assembleur NASM sous Linux et avec les compilateurs C/C++ de Borland et Microsoft sous Windows. Les exemples pour toutes ces plateformes sont disponibles sur mon site Web : http://www.drpaulcarter.com/pcasm.

Vous devez télécharger le code exemple si vous voulez assembler et exécuter la plupart des exemples de ce tutoriel.

Soyez conscient que ce texte ne tente pas de couvrir tous les aspects de la programmation assembleur. L'auteur a essayé de couvrir les sujets les plus importants avec lesquels tous les programmeurs devraient être familiers.

I-B. Remerciements

L'auteur voudrait remercier les nombreux programmeurs qui ont contribué au mouvement Libre/Open Source. Tous les programmes et même ce livre lui-même ont été créés avec des logiciels gratuits. L'auteur voudrait remercier en particulier John S. Fine, Simon Tatham, Julian Hall et les autres développeurs de l'assembleur NASM sur lequel tous les exemples de ce livre sont basés ; DJ Delorie pour le développement du compilateur C/C++ DJGPP utilisé ; les nombreuses personnes qui ont contribué au compilateur GNU gcc sur lequel DJGPP est basé ; Donald Knuth et les autres pour avoir développé les langages composition TEX et LATEX2" qui ont été utilisés pour produire le livre ; Richard Stallman (fondateur de la Free Software Foundation), Linus Torvalds (créateur du noyau Linux) et les autres qui ont créé les logiciels sous-jacents utilisés pour ce travail.

Merci aux personnes suivantes pour leurs corrections :

John S. Fine ; Marcelo Henrique Pinto de Almeida ; Sam Hopkins ; Nick D'Imperio ; Jeremiah Lawrence ; Ed Beroset ; Jerry Gembarowski ; Ziqiang Peng ; Eno Compton ; Josh I Cates ; Mik Miin ; Luke Wallis ; Gaku Ueda ; Brian Heward ; Chad Gorshing ; F. Gotti ; Bob Wilkinson ; Markus Koegel ;Louis Taber.

I-C. Ressources sur Internet

Page de l'auteur : http://www.drpaulcarter.com/.

Page NASM sur SourceForge : http://sourceforge.net/projects/nasm/.

DJGPP : http://www.delorie.com/djgpp.

Assembleur Linux : http://www.linuxassembly.org/.

The Art of Assembly : http://webster.cs.ucr.edu/.

USENET comp.lang.asm.x86

Documentation Intel : http://developer.intel.com/design/Pentium4/documentation.htm.

I-D. Réactions

L'auteur accepte toute réaction sur ce travail.

E-mail : pacman128@gmail.com.

WWW : http://www.drpaulcarter.com/pcasm

II. Introduction

II-A. Systèmes numériques

La mémoire d'un ordinateur est constituée de nombres. Cette mémoire ne stocke pas ces nombres en décimal (base 10). Comme cela simplifie grandement le matériel, les ordinateurs stockent toutes les informations au format binaire (base 2). Tout d'abord, revoyons ensemble le système décimal.

II-A-1. Décimal

Les nombres en base 10 sont constitués de 10 chiffres possibles (0-9).

Chaque chiffre d'un nombre est associé à une puissance de 10 selon sa position dans le nombre. Par exemple :

234 = 2 x 102 + 3 × 101 + 4 × 100

II-A-2. Binaire

Les nombres en base 2 sont composés de deux chiffres possibles (0 et 1). Chaque chiffre est associé à une puissance de 2 selon sa position dans le nombre (un chiffre binaire isolé est appelé bit). Par exemple :

110012 = 1 × 24+ 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20

= 16 + 8 + 1

= 25

Cet exemple montre comment passer du binaire au décimal. Le tableau 1.1 montre la représentation binaire des premiers nombres.

La figure 1.1 montre comment des chiffres binaires individuels (i.e., des bits) sont additionnés. Voici un exemple :

Image non disponible

Si l'on considère la division décimale suivante :

1234 ÷ 10 = 123 r 4

on peut voir que cette division sépare le chiffre le plus à droite du nombre et décale les autres chiffres d'une position vers la droite. Diviser par deux effectue une opération similaire, mais pour les chiffres binaires du nombre.

Considérons la division binaire suivante(1) :

11012 ÷ 102 = 1102 r 1

Cette propriété peut être utilisée pour convertir un nombre décimal en son équivalent binaire comme le montre la Figure 1.2. Cette méthode trouve le chiffre binaire le plus à droite en premier, ce chiffre est appelé le bit le moins signicatif (lsb, least signicant bit). Le chiffre le plus à gauche est appelé le bit le plus signicatif (msb, most signicant bit). L'unité de base de la mémoire consiste en un jeu de 8 bits appelé octet (byte).

Image non disponible

II-A-3. Hexadécimal

Les nombres hexadécimaux utilisent la base 16. L'hexadécimal (ou hexa en abrégé) peut être utilisé comme notation pour les nombres binaires. L'hexa a 16 chiffres possibles. Cela pose un problème, car il n'y a pas de symbole à utiliser pour les chiffres supplémentaires après 9. Par convention, on utilise des lettres pour les représenter. Les 16 chiffres de l'hexa sont 0-9 puis A, B, C, D, E et F. Le chiffre A équivaut à 10 en décimal, B à 11, etc. Chaque chiffre d'un nombre en hexa est associé à une puissance de 16. Par exemple :

2BD16 = 2 × 162 + 11 × 161 + 13 × 160

= 512 + 176 + 13

= 701

Pour convertir un nombre hexa en binaire, convertissez simplement chaque chiffre hexa en un nombre binaire de 4 bits. Par exemple, 24D16 est converti en 0010 0100 11012. Notez que les 0 de tête des 4 bits sont importants ! Si le 0 de tête pour le chiffre du milieu de 24D16 n'est pas utilisé, le résultat est faux. La conversion du binaire vers l'hexa est aussi simple. On effectue la même chose, dans l'autre sens. Convertissez chaque segment de 4 bits du binaire vers l'hexa. Commencez à droite, pas à gauche, du nombre binaire.

Cela permet de s'assurer que le procédé utilise les segments de 4 bits corrects(2)

Exemple :

Image non disponible

Un nombre de 4 bits est appelé quadruplet(nibble). Donc, chaque chiffre hexa correspond à un quadruplet. Deux quadruplets forment un octet et donc un octet peut être représenté par un nombre hexa à deux chiffres. La valeur d'un bit va de 0 à 11111111 en binaire, 0 à FF en hexa et 0 à 255 en décimal.

II-B. Organisation de l'ordinateur

II-B-1. Mémoire

La mémoire est mesurée en kilo-octets ( 210= 1024 octets), mega octets (220= 1 048 576 octets) et gigaoctets (230 = 1 073 741 824 octets).

L'unité mémoire de base est l'octet. Un ordinateur avec 32 mégaoctets de mémoire peut stocker jusqu'à environ 32 millions d'octets d'informations.

Chaque octet en mémoire est étiqueté par un nombre unique appelé son adresse comme le montre la Figure 1.4.

Image non disponible

Souvent, la mémoire est utilisée par bouts plus grands que des octets isolés. Sur l'architecture PC, on a donné des noms à ces portions de mémoire plus grandes, comme le montre le Tableau 1.2.

Image non disponible

Toutes les données en mémoire sont numériques. Les caractères sont stockés en utilisant un code caractère qui fait correspondre des nombres aux caractères. Un des codes caractère les plus connus est appelé ASCII (American Standard Code for Information Interchange, Code Américain Standard pour l'Échange d'Informations). Un nouveau code, plus complet, qui supplante l'ASCII est l'Unicode. Une des différences-clés entre les deux codes est que l'ASCII utilise un octet pour encoder un caractère alors que l'Unicode en utilise deux (ou un mot). Par exemple, l'ASCII fait correspondre l'octet 4116 (6510) au caractère majuscule A ; l'Unicode y fait correspondre le mot 004116. Comme l'ASCII n'utilise qu'un octet, il est limité à 256 caractères différents au maximum(3). L'Unicode étend les valeurs ASCII à des mots et permet de représenter beaucoup plus de caractères. C'est important afin de représenter les caractères de tous les langages du monde.

II-B-2. Le CPU (processeur)

Le processeur (CPU, Central Processing Unit) est le dispositif physique qui exécute les instructions. Les instructions que les processeurs peuvent exécuter sont généralement très simples. Elles peuvent nécessiter que les données sur lesquelles elles agissent soient présentes dans des emplacements de stockage spéciques dans le processeur lui-même appelés registres. Le processeur peut accéder aux données dans les registres plus rapidement qu'aux données en mémoire. Cependant, le nombre de registres d'un processeur est limité, donc le programmeur doit faire attention à n'y conserver que les données actuellement utilisées.

Les instructions que peut exécuter un type de processeur constituent le langage machine. Les programmes machine ont une structure beaucoup plus basique que les langages de plus haut niveau. Les instructions du langage machine sont encodées en nombres bruts, pas en format texte lisible. Un processeur doit être capable de décoder les instructions très rapidement pour fonctionner efficacement. Le langage machine est conçu avec ce but en tête, pas pour être facilement déchiffrable par les humains. Les programmes écrits dans d'autres langages doivent être convertis dans le langage machine programme qui traduit les programmes écrits dans un langage de programmation en langage machine d'une architecture d'ordinateur particulière. En général, chaque type de processeur a son propre langage machine unique. C'est une des raisons pour lesquelles un programme écrit pour Mac ne peut pas être exécuté sur un PC.

Les ordinateurs utilisent une horloge pour synchroniser l'exécution des instructions. L'horloge tourne à une fréquence fixée (appelée vitesse d'horloge). Lorsque vous achetez un ordinateur à 1,5GHz, 1,5GHz est la fréquence de cette horloge.

GHz signifie gigahertz ou un milliard de cycles par seconde. Un processeur à 1,5GHz a 1,5 milliard d'impulsions horloge par seconde.

L'horloge ne décompte pas les minutes et les secondes. Elle bat simplement à un rythme constant. Les composants électroniques du processeur utilisent les pulsations pour effectuer leurs opérations correctement, comme le battement d'un métronome aide à jouer de la musique à un rythme correct. Le nombre de battements (ou, comme on les appelle couramment cycles) que requiert une instruction dépend du modèle et de la génération du processeur. Le nombre de cycles dépend de l'instruction.

II-B-3. La famille des processeurs 80x86

Les PC contiennent un processeur de la famille des Intel 80x86 (ou un clone). Les processeurs de cette famille ont tous des fonctionnalités en commun, y compris un langage machine de base. Cependant, les membres les plus récents améliorent grandement les fonctionnalités.

8088,8086 : ces processeurs, du point de vue de la programmation sont identiques. Ils étaient les processeurs utilisés dans les tout premiers PC. Ils offrent plusieurs registres 16 bits : AX, BX, CX, DX, SI, DI, BP, SP, CS, DS, SS, ES, IP, FLAGS. Ils ne supportent que jusqu'à 1Mo de mémoire et n'opèrent qu'en mode réel. Dans ce mode, un programme peut accéder à n'importe quelle adresse mémoire, même la mémoire des autres programmes ! Cela rend le débogage et la sécurité très difficiles ! De plus, la mémoire du programme doit être divisée en segments. Chaque segment ne peut pas dépasser les 64 Ko.

80286 : ce processeur était utilisé dans les PC de type AT. Il apporte quelques nouvelles instructions au langage machine de base des 8088/86. Cependant, sa principale nouvelle fonctionnalité est le mode protégé 16 bits. Dans ce mode, il peut accéder jusqu'à 16 Mo de mémoire et empêcher les programmes d'accéder à la mémoire des uns et des autres. Cependant, les programmes sont toujours divisés en segments qui ne peuvent pas dépasser les 64 Ko.

80386 : ce processeur a grandement amélioré le 80286. Tout d'abord, il étend la plupart des registres à 32 bits (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP) et ajoute deux nouveaux registres 16 bits : FS et GS.

Image non disponible

Il ajoute également un nouveau mode protégé 32 bits. Dans ce mode, il peut accéder jusqu'à 4 Go de mémoire. Les programmes sont encore divisés en segments, mais maintenant chaque segment peut également faire jusqu'à 4 Go !

80486/Pentium/Pentium Pro : ces membres de la famille 80x86 apportent très peu de nouvelles fonctionnalités. Ils accélèrent principalement l'exécution des instructions.

Pentium MMX : ce processeur ajoute les instructions MMX (Multi Media eXentions) au Pentium. Ces instructions peuvent accélérer des opérations graphiques courantes.

Pentium II : c'est un processeur Pentium Pro avec les instructions MMX (le Pentium III est grossièrement un Pentium II plus rapide).

II-B-4. Regitres 16 bits du 8086

Le processeur 8086 original fournissait quatre registres généraux de 16 bits : AX, BX, CX et DX. Chacun de ces registres peut être décomposé en deux registres de 8 bits. Par exemple, le registre AX pouvait être décomposé en AH et AL comme le montre la Figure 1.5. Le registre AH contient les 8 bits de poids fort de AX et AL contient les 8 bits de poids faible. Souvent, AH et AL sont utilisés comme des registres d'un octet indépendants ; cependant, il est important de réaliser qu'ils ne sont pas indépendants de AX. Changer la valeur de AX changera les valeurs de AL et BL et vice versa. Les registres généraux sont utilisés dans beaucoup de déplacements de données et instructions arithmétiques.

Il y a deux registres d'index de 16 bits : SI et DI. Ils sont souvent utilisés comme des pointeurs, mais peuvent être utilisés pour la plupart des mêmes choses que les registres généraux. Cependant, ils ne peuvent pas être décomposés en registres de 8 bits.

Les registres 16 bits BP et SP sont utilisés pour pointer sur des données dans la pile du langage machine et sont appelés le pointeur de base et le pointeur de pile, respectivement. Nous en reparlerons plus tard.

Les registres 16 bits CS, DS, SS et ES sont des registres de segment. Ils indiquent quelle zone de la mémoire est utilisée pour les différentes parties d'un programme. CS signifie Code Segment, DS Data Segment, SS Stack Segment (segment de pile) et ES Extra Segment. ES est utilisé en tant que registre de segment temporaire. Des détails sur ces registres se trouvent dans les sections II.B.6 et II.B.7.

Le registre de pointeur d'instruction (IP) est utilisé avec le registre CS pour mémoriser l'adresse de la prochaine instruction à exécuter par le processeur.

Normalement, lorsqu'une instruction est exécutée, IP est incrémenté pour pointer vers la prochaine instruction en mémoire.

Le registre FLAGS stocke des informations importantes sur les résultats d'une instruction précédente. Ces résultats sont stockés comme des bits individuels dans le registre. Par exemple, le bit Z est positionné à 1 si le résultat de l'instruction précédente était 0 ou à 0 sinon. Toutes les instructions ne modifient pas les bits dans FLAGS, consultez le tableau dans l'appendice pour voir comment chaque instruction affecte le registre FLAGS.

II-B-5. Registres 32 bits du 80386

Les processeurs 80386 et plus récents ont des registres étendus. Par exemple le registre AX 16 bits est étendu à 32 bits. Pour la compatibilité ascendante, AX fait toujours référence au registre 16 bits et on utilise EAX pour faire référence au registre 32 bits. AX représente les 16 bits de poids faible de EAX tout comme AL représente les 8 bits de poids faible de AX (et de EAX). Il n'y a pas moyen d'accéder aux 16 bits de poids fort de EAX directement. Les autres registres étendus sont EBX, ECX, EDX, ESI et EDI.

La plupart des autres registres sont également étendus. BP devient EBP ; SP devient ESP ; FLAGS devient EFLAGS et IP devient EIP. Cependant, contrairement aux registres généraux et d'index, en mode protégé 32 bits (dont nous parlons plus loin) seules les versions étendues de ces registres sont utilisées.

Les registres de segment sont toujours sur 16 bits dans le 80386. Il y a également deux registres de segment supplémentaires : FS et GS. Leurs noms n'ont pas de signification particulière. Ce sont des segments temporaires supplémentaires (comme ES).

Une des définitions du terme mot se réfère à la taille des registres de données du processeur. Pour la famille du 80x86, le terme est désormais un peu confus. Dans le Tableau 1.2, on voit que le terme mot est déni comme faisant 2 octets (ou 16 bits). Cette signification lui a été attribuée lorsque le premier 8086 est apparu. Lorsque le 80386 a été développé, il a été décidé de laisser la définition de mot inchangée, même si la taille des registres avait changé.

II-B-6. Mode réel

En mode réel, la mémoire est limitée à seulement un mégaoctet (220 octets).

Alors d'où vient l'infâme limite des 640 Ko du DOS ? Le BIOS requiert une partie des 1 Mo pour son propre code et pour les périphériques matériels comme l'écran.

Les adresses valides vont de 00000 à FFFFF (en hexa). Ces adresses nécessitent un nombre sur 20 bits. Cependant, un nombre de 20 bits ne tiendrait dans aucun des registres 16 bits du 8086. Intel a résolu le problème, en utilisant deux valeurs de 16 bits pour déterminer une adresse. La première valeur de 16 bits est appelée le sélecteur. Les valeurs du sélecteur doivent être stockées dans des registres de segment. La seconde valeur de 16 bits est appelée le déplacement (offset). L'adresse physique identifiée par un couple sélecteur/déplacement 32 bits est calculée par la formule

16 * sélecteur + déplacement

Multiplier par 16 en hexa est facile, il suffit d'ajouter un 0 à la droite du nombre. Par exemple, l'adresse physique référencée par 047C:0048 est obtenue de la façon suivante :

047C0

+0048

04808

De fait, la valeur du sélecteur est un numéro de paragraphe (voir Tableau 1.2).

Les adresses réelles segmentées ont des inconvénients :

  • une seule valeur de sélecteur peut seulement référencer 64Ko de mémoire (la limite supérieure d'un déplacement de 16 bits). Que se passe-t-il si un programme a plus de 64 Ko de code ? Une seule valeur de CS ne peut pas être utilisée pour toute l'exécution du programme. Le programme doit être divisé en sections (appelées segments) de moins de 64 Ko. Lorsque l'exécution passe d'un segment à l'autre, la valeur de CS doit être changée. Des problèmes similaires surviennent avec de grandes quantités de données et le registre DS. Cela peut être très gênant ! ;
  • chaque octet en mémoire n'a pas une adresse segmentée unique. L'adresse physique 04808 peut être référencée par 047C:0048, 047D:0038, 047E:0028 ou 047B:0058. Cela complique la comparaison d'adresses segmentées.

II-B-7. Mode protégé 16 bits

Dans le mode protégé 16 bits du 80286, les valeurs du sélecteur sont interprétées de façon totalement différente par rapport au mode réel. En mode réel, la valeur d'un sélecteur est un numéro de paragraphe en mémoire.

En mode protégé, un sélecteur est un indice dans un tableau de descripteurs. Dans les deux modes, les programmes sont divisés en segments. En mode réel, ces segments sont à des positions fixes en mémoire et le sélecteur indique le numéro de paragraphe auquel commence le segment. En mode protégé, les segments ne sont pas à des positions fixes en mémoire physique. De fait, ils n'ont même pas besoin d'être en mémoire du tout !

Le mode protégé utilise une technique appelée mémoire virtuelle. L'idée de base d'un système de mémoire virtuelle est de ne garder en mémoire que les programmes et les données actuellement utilisés. Le reste des données et du code sont stockés temporairement sur le disque jusqu'à ce qu'on ait à nouveau besoin d'eux. Dans le mode protégé 16 bits, les segments sont déplacés entre la mémoire et le disque selon les besoins. Lorsqu'un segment est rechargé en mémoire depuis le disque, il est très probable qu'il sera à un endroit en mémoire différent de celui où il était avant d'être placé sur le disque.

Tout ceci est effectué de façon transparente par le système d'exploitation. Le programme n'a pas à être écrit différemment pour que la mémoire virtuelle fonctionne.

En mode protégé, chaque segment est assigné à une entrée dans un tableau de descripteurs. Cette entrée contient toutes les informations dont le système a besoin à propos du segment. Ces informations indiquent : s'il est actuellement en mémoire ; s'il est en mémoire, où il se trouve ; les droits d'accès (p.e., lecture seule). L'indice de l'entrée du segment est la valeur du sélecteur stockée dans les registres de segment.

Un journaliste PC bien connu a baptisé le processeur 286 « cerveau mort » (brain dead).

Un gros inconvénient du mode protégé 16 bits est que les déplacements sont toujours des quantités sur 16 bits. En conséquence, les tailles de segment sont toujours limitées au plus à 64 Ko. Cela rend l'utilisation de grands tableaux problématique.

II-B-8. Mode protégé 32 bits

Le 80386 a introduit le mode protégé 32 bits. Il y a deux différences majeures entre les modes protégés 32 bits du 386 et 16 bits du 286 :

1. Les déplacements sont étendus à 32 bits. Cela permet à un déplacement d'aller jusqu'à 4 milliards. Ainsi, les segments peuvent avoir des tailles jusqu'à 4 Go ;

2. Les segments peuvent être divisés en unités plus petites de 4 Ko appelées pages. Le système de mémoire virtuelle fonctionne maintenant avec des pages plutôt qu'avec des segments. Cela implique que seules certaines parties d'un segment peuvent être présentes en mémoire à un instant donné. En mode 16 bits du 286, soit le segment en entier est en mémoire, soit rien n'y est. Ce qui n'aurait pas été pratique avec les segments plus grands que permet le mode 32 bits.

Dans Windows 3.x, le mode standard fait référence au mode protégé 16 bits du 286 et le mode amélioré (enhanced) fait référence au mode 32 bits.

Windows 9X, Windows NT/2000/XP, OS/2 et Linux fonctionnent tous en mode protégé 32 bits paginé.

II-B-9. Interruptions

Quelques fois, le flot ordinaire d'un programme doit être interrompu pour traiter des évènements qui requièrent une réponse rapide. Le matériel d'un ordinateur offre un mécanisme appelé interruptions pour gérer ces évènements.

Par exemple, lorsqu'une souris est déplacée, la souris interrompt le programme en cours pour gérer le déplacement de la souris (pour déplacer le curseur, etc.). Les interruptions provoquent le passage du contrôle à un gestionnaire d'interruptions. Les gestionnaires d'interruptions sont des routines qui traitent une interruption. Chaque type d'interruption est assignée à un nombre entier. Au début de la mémoire physique, réside un tableau de vecteurs d'interruptions qui contient les adresses segmentées des gestionnaires d'interruptions. Le numéro d'une interruption est essentiellement un indice dans ce tableau.

Les interruptions externes proviennent de l'extérieur du processeur (la souris est un exemple de ce type). Beaucoup de périphériques d'E/S soulèvent des interruptions (p.e., le clavier, le timer, les lecteurs de disque, le CD-ROM et les cartes son). Les interruptions internes sont soulevées depuis le processeur, à cause d'une erreur ou d'une instruction d'interruption.

Les interruptions erreur sont également appelées traps. Les interruptions générées par l'instruction d'interruption sont également appelées interruptions logicielles. Le DOS utilise ce type d'interruption pour implémenter son API (Application Programming Interface). Les systèmes d'exploitation plus récents (comme Windows et Unix) utilisent une interface basée sur C(4).

Beaucoup de gestionnaires d'interruptions redonnent le contrôle au programme interrompu lorsqu'ils se terminent. Ils restaurent tous les registres aux valeurs qu'ils avaient avant l'interruption. Ainsi, le programme interrompu s'exécute comme si rien n'était arrivé (excepté qu'il perd quelques cycles processeur). Les traps ne reviennent généralement jamais. Souvent, elles arrêtent le programme.

II-C. Langage assembleur

II-C-1. Langage machine

Chaque type de processeur comprend son propre langage machine. Les instructions dans le langage machine sont des nombres stockés sous la forme d'octets en mémoire. Chaque instruction a son propre code numérique unique appelé code d'opération ou opcode (operation code) en raccourci. Les instructions des processeurs 80x86 varient en taille. L'opcode est toujours au début de l'instruction. Beaucoup d'instructions comprennent également les données (p.e. des constantes ou des adresses) utilisées par l'instruction.

Le langage machine est très difficile à programmer directement. Déchiffrer la signification d'instructions codées numériquement est fatigant pour des humains. Par exemple, l'instruction qui dit d'ajouter les registres EAX et EBX et de stocker le résultat dans EAX est encodée par les codes hexadécimaux suivants :

03 C3

C'est très peu clair. Heureusement, un programme appelé un assembleur peut faire ce travail laborieux à la place du programmeur.

II-C-2. Langage d'assembleur

Un programme en langage d'assembleur est stocké sous la forme de texte (comme un programme dans un langage de plus haut niveau). Chaque instruction assembleur représente exactement une instruction machine. Par exemple, l'instruction d'addition décrite ci-dessus serait représentée en langage assembleur comme suit :

add eax, ebx

Ici, la signification de l'instruction est beaucoup plus claire qu'en code machine.

Le mot add est un mnémonique pour l'instruction d'addition. La forme générale d'une instruction assembleur est :

mnémonique opérande(s)

Un assembleur est un programme qui lit un fichier texte avec des instructions assembleur et convertit l'assembleur en code machine. Les compilateurs sont des programmes qui font des conversions similaires pour les langages de programmation de haut niveau. Un assembleur est beaucoup plus simple qu'un compilateur.

Cela a pris plusieurs années aux scientifiques de l'informatique pour concevoir le simple fait d'écrire un compilateur !

Chaque instruction du langage d'assembleur représente directement une instruction machine. Les instructions d'un langage de plus haut niveau sont beaucoup plus complexes et peuvent requérir beaucoup d'instructions machine.

Une autre différence importante entre l'assembleur et les langages de haut niveau est que comme chaque type de processeur a son propre langage machine, il a également son propre langage d'assemblage. Porter des programmes assembleur entre différentes architectures d'ordinateur est beaucoup plus difficile qu'avec un langage de haut niveau.

Les exemples de ce livre utilisent le Netwide Assembler ou NASM en

raccourci. Il est disponible gratuitement sur Internet (voyez la préface pour l'URL). Des assembleurs plus courants sont l'Assembleur de Microsoft (MASM) ou l'Assembleur de Borland (TASM). Il y a quelques différences de syntaxe entre MASM/TASM et NASM.

II-C-3. Opérandes d'instruction

Les instructions en code machine ont un nombre et un type variables d'opérandes ; cependant, en général, chaque instruction a un nombre fixé d'opérandes (0 à 3). Les opérandes peuvent avoir les types suivants :

registre : ces opérandes font directement référence au contenu des registres du processeur ;

mémoire : ils font référence aux données en mémoire. L'adresse de la donnée peut être une constante codée en dur dans l'instruction ou calculée en utilisant les valeurs des registres. Les adresses sont toujours des déplacements relatifs au début d'un segment ;

immédiat : ce sont des valeurs fixes qui sont listées dans l'instruction elle-même. Elles sont stockées dans l'instruction (dans le segment de code), pas dans le segment de données ;

implicite : ces opérandes ne sont pas entrés explicitement. Par exemple, l'instruction d'incrémentation ajoute 1 à un registre ou à la mémoire. Le 1 est implicite.

II-C-4. Instructions de base

L'instruction la plus basique est l'instruction MOV. Elle déplace les données d'un endroit à un autre (comme l'opérateur d'assignement dans un langage de haut niveau). Elle prend deux opérandes :

Sélectionnez
mov dest, src

La donnée spécifiée par src est copiée vers dest. Une restriction est que les deux opérandes ne peuvent pas être tous deux des opérandes mémoire.

Cela nous montre un autre caprice de l'assembleur. Il y a souvent des règles quelque peu arbitraires sur la façon dont les différentes instructions sont utilisées. Les opérandes doivent également avoir la même taille. La valeur de AX ne peut pas être stockée dans BL.

Voici un exemple (les points-virgules marquent un commentaire) :

Sélectionnez
mov eax, 3 ; stocke 3 dans le registre EAX (3 est un operande immediat)

mov bx, ax ; stocke la valeur de AX dans le registre BX

L'instruction ADD est utilisée pour additionner des entiers.

Sélectionnez
add eax, 4 ; eax = eax + 4

add al, ah ; al = al + ah

L'instruction SUB soustrait des entiers.

Sélectionnez
sub bx, 10 ; bx = bx - 10

sub ebx, edi ; ebx = ebx - edi

Les instructions INC et DEC incrémentent ou décrémentent les valeurs de 1. Comme le 1 est un opérande implicite, le code machine pour INC et DEC est plus petit que celui des instructions ADD et SUB équivalentes.

Sélectionnez
inc ecx ; ecx++

dec dl ; dl--

II-C-5. Directives

Une directive est destinée à l'assembleur, pas au processeur. Les directives sont généralement utilisées pour indiquer à l'assembleur de faire quelque chose ou pour l'informer de quelque chose. Elles ne sont pas traduites en code machine. Les utilisations courantes des directives sont :

  • la définition de constantes ;
  • la définition de mémoire pour stocker des données ;
  • grouper la mémoire en segment ;
  • inclure des codes source de façon conditionnelle ;
  • inclure d'autres fichiers.

Le code NASM est analysé par un préprocesseur, exactement comme en C. Il y a beaucoup de commandes identiques à celles du préprocesseur C.

Cependant, les directives du préprocesseur NASM commencent par un % au lieu d'un # comme en C.

II-C-5-a. La directive equ

La directive equ peut être utilisée pour définir un symbole. Les symboles sont des constantes nommées qui peuvent être utilisées dans le programme assembleur. Le format est le suivant :

Sélectionnez
symbole equ valeur

Les valeurs des symboles ne peuvent pas être redéfinies plus tard.

II-C-5-b. La directive ?ne

Cette directive est semblable à la directive #define du C. Elle est le plus souvent utilisée pour définir des macros, exactement comme en C.

Sélectionnez
?fine SIZE 100
mov eax, SIZE
Image non disponible

Le code ci-dessus définit une macro appelée SIZE et montre son utilisation dans une instruction MOV. Les macros sont plus flexibles que les symboles de deux façons. Elles peuvent être redéfinies et peuvent être plus que de simples nombres constants.

II-C-5-c. Directives de données

Les directives de données sont utilisées dans les segments de données pour réserver de la place en mémoire. Il y a deux façons de réserver de la mémoire. La première ne fait qu'allouer la place pour les données ; a seconde alloue la place et donne une valeur initiale. La première méthode utilise une des directives RESX. Le X est remplacé par une lettre qui détermine la taille de l'objet (ou des objets) qui sera stocké. Le Tableau 1.3 montre les valeurs possibles.

La seconde méthode (qui définit une valeur initiale) utilise une des directives DX. Les lettres X sont les mêmes que celles de la directive RESX.

Il est très courant de marquer les emplacements mémoire avec des labels (étiquettes). Les labels permettent de faire référence facilement aux emplacements mémoire dans le code. Voici quelques exemples :

Sélectionnez
L1 db 0 ; octet libelle L1 avec une valeur initiale de 0
L2 dw 1000 ; mot labelle L2 avec une valeur initiale de 1000
L3 db 110101b ; octet initialise a la valeur binaire 110101 (53 en decimal)
L4 db 12h ; octet initialise a la valeur hexa 12 (18 en decimal)
L5 db 17o ; octet initialise a la valeur octale 17 (15 en decimal)
L6 dd 1A92h ; double mot initialise a la valeur hexa 1A92
L7 resb 1 ; 1 octet non initialise
L8 db "A" ; octet initialise avec le code ASCII du A (65)

Les doubles et simples quotes sont traitées de la même façon. Les définitions de données consécutives sont stockées séquentiellement en mémoire. C'est-à-dire que le mot L2 est stocké immédiatement après L1 en mémoire. Des séquences de mémoire peuvent également être définies.

Sélectionnez
L9 db 0, 1, 2, 3 ; definit 4 octets
L10 db "w", "o", "r", 'd', 0 ; definit une chaine C = "word"
L11 db 'word', 0 ; idem L10

La directive DD peut être utilisée pour définir à la fois des entiers et des constantes à virgule flottante en simple précision(5). Cependant, la directive DQ ne peut être utilisée que pour définir des constantes à virgule flottante en double précision.

Pour les grandes séquences, la directive de NASM TIMES est souvent utile. Cette directive répète son opérande un certain nombre de fois. Par exemple :

Sélectionnez
L12 times 100 db 0 ; equivalent a 100 (db 0)
L13 resw 100 ; reserve de la place pour 100 mots

Souvenez-vous que les labels peuvent être utilisés pour faire référence à des données dans le code. Il y a deux façons d'utiliser les labels. Si un label simple est utilisé, il fait référence à l'adresse (ou offset) de la donnée.

Si le label est placé entre crochets ([ ]), il est interprété comme la donnée à cette adresse. En d'autres termes, il faut considérer un label comme un pointeur vers la donnée et les crochets déréférencent le pointeur, exactement comme l'astérisque en C (MASM/TASM utilisent une convention différente).

En mode 32 bits, les adresses sont sur 32 bits. Voici quelques exemples :

Sélectionnez
  1. mov al, [L1] ; Copie l'octet situe en L1 dans AL
  2. mov eax, L1 ; EAX = addresse de l'octet en L1
  3. mov [L1], ah ; copie AH dans l'octet en L1
  4.  mov eax, [L6] ; copie le double mot en L6 dans EAX
  5.  add eax, [L6] ; EAX = EAX + double mot en L6
  6. add [L6], eax ; double mot en L6 += EAX
  7. mov al, [L6] ; copie le premier octet du double mot en L6 dans AL

La ligne 7 de cet exemple montre une propriété importante de NASM. L'assembleur ne garde pas de trace du type de données auquel se réfère le label. C'est au programmeur de s'assurer qu'il (ou elle) utilise un label correctement.

Plus tard, il sera courant de stocker des adresses dans les registres et utiliser les registres comme un pointeur en C. Là encore, aucune vérification n'est faite pour savoir si le pointeur est utilisé correctement. À ce niveau, l'assembleur est beaucoup plus sujet à erreur que le C.

Considérons l'instruction suivante :

Sélectionnez
  1. mov [L6], 1 ; stocke 1 en L6

Cette instruction produit une erreur operation size not specified. Pourquoi ?

Parce que l'assembleur ne sait pas s'il doit considérer 1 comme un octet, un mot ou un double mot. Pour réparer cela, il faut ajouter un spécificateur de taille :

Sélectionnez
  1. mov dword [L6], 1 ; stocke 1 en L6

Cela indique à l'assembleur de stocker un 1 dans le double mot qui commence en L6. Les autres spécificateurs de taille sont : BYTE, WORD, QWORD et TWORD(6).

II-C-6. Entrées et sorties

Les entrées et sorties sont des activités très dépendantes du système. Cela implique un interfaçage avec le matériel. Les langages de plus haut niveau, comme C, fournissent des bibliothèques standard de routines pour une interface de programmation simple, uniforme pour les E/S. Les langages d'assembleur n'ont pas de bibliothèque standard. Ils doivent soit accéder directement au matériel (avec une opération privilégiée en mode protégé) ou utiliser les routines de bas niveau éventuellement fournies par le système d'exploitation.

Il est très courant pour les routines assembleur d'être interfacées avec du C. Un des avantages de cela est que le code assembleur peut utiliser les routines d'E/S de la bibliothèque standard du C. Cependant, il faut connaître les règles de passage des informations aux routines que le C utilise.

Ces règles sont trop compliquées pour en parler ici (nous en parlerons plus tard !). Pour simplifier les E/S, l'auteur a développé ses propres routines qui masquent les règles complexes du C et fournissent une interface beaucoup plus simple. Le Tableau 1.4 décrit les routines fournies. Toutes les routines préservent les valeurs de tous les registres, excepté les routines read. Ces routines modifient la valeur du registre EAX. Pour utiliser ces routines, il faut inclure un fichier contenant les informations dont l'assembleur a besoin pour les utiliser. Pour inclure un fichier dans NASM, utilisez la directive du préprocesseur %include. La ligne suivante inclut le fichier requis par les routines d'E/S de l'auteur(7) :

Sélectionnez
  1. %include "asm_io.inc"

Pour utiliser une de ces routines d'affichage, il faut charger EAX avec la valeur correcte et utiliser une instruction CALL pour l'invoquer. L'instruction CALL est équivalente à un appel de fonction dans un langage de haut niveau.

Elle saute à une autre portion de code, mais revient à son origine une fois la routine terminée. Le programme d'exemple ci-dessous montre plusieurs exemples d'appel à ces routines d'E/S.

Image non disponible

II-C-7. Débogage

La bibliothèque de l'auteur contient également quelques routines utiles pour déboguer les programmes. Ces routines de débogage affichent des informations sur l'état de l'ordinateur sans le modifier. Ces routines sont en fait des macros qui sauvegardent l'état courant du processeur puis font appel à une sous-routine. Les macros sont définies dans le fichier asm_io.inc dont nous avons parlé plus haut. Les macros sont utilisées comme des instructions ordinaires. Les opérandes des macros sont séparés par des virgules.

Il y a quatre routines de débogage nommées dump_regs, dump_mem, dump_stack et dump_math ; elles affichent respectivement les valeurs des registres, de la mémoire, de la pile et du coprocesseur arithmétique.

dump_regs : cette macro affiche les valeurs des registres (en hexadécimal) de l'ordinateur sur stdout (i.e. l'écran). Elle affiche également les bits positionnés dans le registre FLAGS(8). Par exemple, si le drapeau zéro (zero ag) est à 1, ZF est affiché. S'il est à 0, il n'est pas affiché. Elle prend en argument un entier qui est affiché également. Cela peut aider à distinguer la sortie de différentes commandes dump_regs.

dump_mem : cette macro affiche les valeurs d'une région de la mémoire (en hexadécimal et également en caractères ASCII). Elle prend trois arguments séparés par des virgules. Le premier est un entier utilisé pour étiqueter la sortie (comme l'argument de dump_regs). Le second argument est l'adresse à afficher (cela peut être un label). Le dernier argument est le nombre de paragraphes de 16 octets à afficher après l'adresse. La mémoire affichée commencera au premier multiple de paragraphe avant l'adresse demandée.

dump_stack : cette macro affiche les valeurs de la pile du processeur (la pile sera présentée dans le Chapitre V). La pile est organisée en doubles-mots et cette routine les affiche de cette façon. Elle prend trois arguments délimités par des virgules. Le premier est un entier (comme pour

dump_regs). Le second est le nombre de doubles-mots à afficher après l'adresse que le registre EBP contient et le troisième argument est le nombre de doubles-mots à afficher avant l'adresse contenue dans EBP.

dump_math : cette macro affiche les valeurs des registres du coprocesseur arithmétique. Elle ne prend qu'un entier en argument qui est utilisé pour étiqueter la sortie comme le fait l'argument de dump_regs.

II-D. Créer un programme

Aujourd'hui, il est très peu courant de créer un programme autonome écrit complètement en langage assembleur. L'assembleur est habituellement utilisé pour optimiser certaines routines critiques. Pourquoi ? Il est beaucoup plus simple de programmer dans un langage de plus haut niveau qu'en assembleur.

De plus, utiliser l'assembleur rend le programme très dur à porter sur d'autres plateformes. En fait, il est rare d'utiliser l'assembleur tout court.

Alors, pourquoi apprendre l'assembleur ?

1. Quelques fois, le code écrit en assembleur peut être plus rapide et plus compact que le code généré par un compilateur.

2. L'assembleur permet l'accès à des fonctionnalités matérielles du système directement qu'il pourrait être difficile ou impossible à utiliser depuis un langage de plus haut niveau.

3. Apprendre à programmer en assembleur aide à acquérir une compréhension plus profonde de la façon dont fonctionne un ordinateur.

4. Apprendre à programmer en assembleur aide à mieux comprendre comment les compilateurs et les langages de haut niveau comme C fonctionnent.

Ces deux derniers points démontrent qu'apprendre l'assembleur peut être utile même si on ne programme jamais dans ce langage plus tard. En fait, l'auteur programme rarement en assembleur, mais il utilise les leçons qu'il en a tirées tous les jours.

II-D-1. Premier programme

Les programmes qui suivront dans ce texte partiront tous du programme de lancement en C de la Figure 1.6.

Image non disponible

Il appelle simplement une autre fonction nommée asm_main. C'est la routine qui sera écrite en assembler proprement dit. Il y a plusieurs avantages à utiliser un programme de lancement en C. Tout d'abord, cela laisse le système du C initialiser le programme de façon à fonctionner correctement en mode protégé. Tous les segments et les registres correspondants seront initialisés par le C. L'assembleur n'aura pas à se préoccuper de cela. Ensuite, la bibliothèque du C pourra être utilisée par le code assembleur. Les routines d'E/S de l'auteur en tirent parti. Elles utilisent les fonctions d'E/S du C (printf, etc.). L'exemple suivant montre un programme assembleur simple.

first.asm
Sélectionnez
asm
1 ; fichier : first.asm
2 ; Premier programme assembleur. Ce programme attend la saisie de deux
3 ; entiers et affiche leur somme.
4 ;
5 ; Pour creer l'executable en utilisant djgpp :
6 ; nasm -f coff first.asm
7 ; gcc -o first first.o driver.c asm_io.o
8
9 %include "asm_io.inc"
10 ;
11 ; Les donnees initialisees sont placees dans le segment .data
12 ;
13 segment .data
14 ;
15 ; Ces labels referencent les chaines utilisees pour l'affichage
16 ;
17 prompt1 db "Entrez un nombre : ", 0 ; N'oubliez pas le 0 final
18 prompt2 db "Entrez un autre nombre : ", 0
19 outmsg1 db "Vous avez entre ", 0
20 outmsg2 db " et ", 0
21 outmsg3 db ", leur somme vaut ", 0
22
23 ;
24 ; Les donnees non initialisees sont placees dans le segment .bss
25 ;
26 segment .bss
27 ;
28 ; Ces labels referencent les doubles-mots utilises pour stocker les entrees
29 ;
30 input1 resd 1
31 input2 resd 1
32
33 ;
34 ; Le code est place dans le segment .text
35 ;
36 segment .text
37 global _asm_main
38 _asm_main :
39 enter 0,0 ; initialisation
40 pusha
41
42 mov eax, prompt1 ; affiche un message
43 call print_string
44
45 call read_int ; lit un entier
46 mov [input1], eax ; le stocke dans input1
47
48 mov eax, prompt2 ; affiche un message
49 call print_string
50
51 call read_int ; lit un entier
52 mov [input2], eax ; le stocke dans input2
53
54 mov eax, [input1] ; eax = dword en input1
55 add eax, [input2] ; eax += dword en input2
56 mov ebx, eax ; ebx = eax
57
58 dump_regs 1 ; affiche les valeurs des registres
59 dump_mem 2, outmsg1, 1 ; affiche le contenu de la memoire
60 ;
61 ; Ce qui suit affiche le message resultat en plusieurs etapes
62 ;
63 mov eax, outmsg1
64 call print_string ; affiche le premier message
65 mov eax, [input1]
66 call print_int ; affiche input1
67 mov eax, outmsg2
68 call print_string ; affiche le second message
69 mov eax, [input2]
70 call print_int ; affiche input2
71 mov eax, outmsg3
72 call print_string ; affiche le troisieme message
73 mov eax, ebx
74 call print_int ; affiche la somme (ebx)
75 call print_nl ; affiche une nouvelle ligne
76
77 popa
78 mov eax, 0 ; retourne dans le programme C
79 leave
80 ret

La ligne 13 du programme définit une section qui spécifie la mémoire à stocker dans le segment de données (dont le nom est .data). Seules les données initialisées doivent être définies dans ce segment.

Dans les lignes 17 à 21, plusieurs chaînes sont déclarées. Elles seront affichées avec la bibliothèque C et doivent donc se terminer par un caractère null (code ASCII 0). Souvenez-vous qu'il y a une grande différence entre 0 et '0'.

Les données non initialisées doivent être déclarées dans le segment bss (appelé .bss à la ligne 26). Ce segment tient son nom d'un vieil opérateur assembleur UNIX qui signifiait block started by symbol. Il y a également un segment de pile. Nous en parlerons plus tard.

Le segment de code est appelé .text historiquement. C'est là que les instructions sont placées. Notez que le label de code pour la routine main (ligne 38) a un préfixe de soulignement. Cela fait partie de la convention d'appel C. Cette convention spécifie les règles que le C utilise lorsqu'il compile le code. Il est très important de connaître cette convention lorsque l'on interface du C et de l'assembleur. Plus loin, la convention sera présentée dans son intégralité ; cependant, pour l'instant, il suffit de savoir que tous les symboles C (i.e., les fonctions et les variables globales) ont un préfixe de soulignement qui leur est ajouté par le compilateur C (cette règle s'applique spécifiquement pour DOS/Windows, le compilateur C Linux n'ajoute rien du tout aux noms des symboles).

La directive global ligne 37 indique à l'assembleur de rendre le label _asm_main global. Contrairement au C, les labels ont une portée interne par défaut. Cela signifie que seul le code du même module peut utiliser le label.

La directive global donne au(x) label(s) spécifié(s) une portée externe. Ce type de label peut être accédé par n'importe quel module du programme.

Le module asm_io déclare les labels print_int, et.al. comme étant globaux. C'est pourquoi l'on peut les utiliser dans le module first.asm.

II-D-2. Dépendance vis-à-vis du compilateur

Le code assembleur ci-dessus est spécifique au compilateur C/C++ GNU(9) gratuit DJGPP(10).

Ce compilateur peut être téléchargé gratuitement depuis Internet. Il nécessite un PC à base de 386 ou plus et tourne sous DOS, Windows 95/98 ou NT. Ce compilateur utilise des fichiers objet au format COFF (Common Object File Format). Pour assembler le fichier au format COFF, utilisez l'option -f coff avec nasm (comme l'indiquent les commentaires du code). L'extension du fichier objet sera o.

Le compilateur C Linux est également un compilateur GNU. Pour convertir le code ci-dessus afin qu'il tourne sous Linux, retirez simplement les préfixes de soulignement aux lignes 37 et 38. Linux utilise le format ELF (Executable and Linkable Format) pour les fichiers objet. Utilisez l'option -f elf pour Linux. Il produit également un fichier objet avec l'extension o.

Les fichiers spécifiques aucompilateur, disponibles sur le site de l'auteur, ont déjà été modifiés pour fonctionner avec le compilateur approprié.

Borland C/C++ est un autre compilateur populaire. Il utilise le format Microsoft OMF pour les fichiers objets. Utilisez l'option -f obj pour les compilateurs Borland. L'extension du fichier objet sera obj. Le format OMF utilise des directives segment différentes de celles des autres formats objet.

Le segment data (ligne 13) doit être changé en :

Sélectionnez
segment _DATA public align=4 class=DATA use32

Le segment bss (ligne 26) doit être changé en :

Sélectionnez
segment _BSS public align=4 class=BSS use32

Le segment text (ligne 36) doit être changé en :

Sélectionnez
segment _TEXT public align=1 class=CODE use32

De plus, une nouvelle ligne doit être ajoutée avant la ligne 36 :

Sélectionnez
group DGROUP _BSS _DATA

Le compilateur Microsoft C/C++ peut utiliser soit le format OMF, soit le format Win32 pour les fichiers objet (si on lui passe un format OMF, il convertit les informations au format Win32 en interne). Le format Win32 permet de définir les segments comme pour DJGPP et Linux. Utilisez l'option -f win32 pour produire un fichier objet dans ce format. L'extension du fichier objet sera obj.

II-D-3. Assembler le code

La première étape consiste à assembler le code. Depuis la ligne de commande, saisissez :

Sélectionnez
nasm -f format-objet first.asm

où format-objet est soit co , soit elf , soit obj soit win32 selon le compilateur C utilisé (souvenez-vous que le fichier source doit être modifié pour Linux et pour Borland).

II-D-4. Compiler le code C

Compilez le fichier driver.c en utilisant un compilateur C. Pour DJGPP, utilisez :

Sélectionnez
gcc -c driver.c

L'option -c signifie de compiler uniquement, sans essayer de lier. Cette option fonctionne à la fois sur les compilateurs Linux, Borland et Microsoft.

II-D-5. Lier les fichiers objet

L'édition de liens est le procédé qui consiste à combiner le code machine et les données des fichiers objet et des bibliothèques afin de créer un fichier exécutable. Comme nous allons le voir, ce processus est compliqué.

Le code C nécessite la bibliothèque standard du C et un code de démarrage spécial afin de s'exécuter. Il est beaucoup plus simple de laisser le compilateur C appeler l'éditeur de liens avec les paramètres corrects que d'essayer d'appeler l'éditeur de liens directement. Par exemple, pour lier le code du premier programme en utilisant DJGPP, utilisez :

Sélectionnez
gcc -o first driver.o first.o asm_io.o

Cela crée un exécutable appelé first.exe (ou juste first sous Linux).

Avec Borland, on utiliserait :

Sélectionnez
bcc32 first.obj driver.obj asm_io.obj

Borland utilise le nom du premier fichier afin de déterminer le nom de l'exécutable. Donc, dans le cas ci-dessus, le programme s'appellera first.exe.

Il est possible de combiner les étapes de compilation et d'édition de liens.

Par exemple :

Sélectionnez
gcc -o first driver.c first.o asm_io.o

Maintenant gcc compilera driver.c puis liera.

II-D-6. Comprendre un listing assembleur

L'option -l fichier-listing peut être utilisée pour indiquer à nasm de créer un fichier listing avec le nom donné. Ce fichier montre comment le code a été assemblé. Voici comment les lignes 17 et 18 (dans le segment data) apparaissent dans le fichier listing (les numéros de ligne sont dans le

fichier listing ; cependant, notez que les numéros de ligne dans le fichier source peuvent ne pas être les mêmes).

Sélectionnez
48 00000000 456E7465722061206E- prompt1 db "Entrez un nombre : ", 0
49 00000009 756D6265723A2000
50 00000011 456E74657220616E6F- prompt2 db "Entrez un autre nombre : ", 0
51 0000001A 74686572206E756D62-
52 00000023 65723A2000

Les nombres diffèrent sur la version française, car ce ne sont pas les mêmes caractères.

La première colonne de chaque ligne est le numéro de la ligne et la seconde est le déplacement (en hexa) de la donnée dans le segment. La troisième colonne montre les données hexa brutes qui seront stockées. Dans ce cas, les données hexa correspondent aux codes ASCII. Enfin, le texte du fichier source est affiché sur la droite. Les déplacements listés dans la seconde colonne ne sont très probablement pas les déplacements réels auxquels les données seront placées dans le programme terminé. Chaque module peut définir ses propres labels dans le segment de données (et les autres segments également). Lors de l'étape d'édition de liens (voir Section II.D.5), toutes ces définitions de labels de segment de données sont combinées pour ne former qu'un segment de données. Les déplacements finals sont alors calculés par l'éditeur de liens.

Voici une petite portion (lignes 54 à 56 du fichier source) du segment text dans le fichier listing :

Sélectionnez
94 0000002C A1[00000000] mov eax, [input1]
95 00000031 0305[04000000] add eax, [input2]
96 00000037 89C3 mov ebx, eax

La troisième colonne montre le code machine généré par l'assembleur. Souvent le code complet pour une instruction ne peut pas encore être calculé.

Par exemple, ligne 94, le déplacement (ou l'adresse) de input1 n'est pas connu avant que le code ne soit lié. L'assembleur peut calculer l'opcode pour l'instruction mov (qui, d'après le listing, est A1), mais il écrit le déplacement entre crochets, car la valeur exacte ne peut pas encore être calculée. Dans ce cas, un déplacement temporaire de 0 est utilisé, car input1 est au début du segment bss déclaré dans ce fichier. Souvenez-vous que cela ne signifie pas qu'il sera au début du segment bss final du programme. Lorsque le code est lié, l'éditeur de liens insérera le déplacement correct à la place.

D'autres instructions, comme ligne 96, ne font référence à aucun label. Dans ce cas, l'assembleur peut calculer le code machine complet.

Représentations Big et Little Endian

Endian est prononcé comme indian.

Si l'on regarde attentivement la ligne 95, quelque chose semble très bizarre à propos du déplacement entre crochets dans le code machine. Le label input2 est au déplacement 4 (comme défini dans ce fichier) ; cependant, le déplacement qui apparaît en mémoire n'est pas 00000004, mais 04000000.

Pourquoi ? Des processeurs différents stockent les entiers multioctets dans des ordres différents en mémoire. Il y a deux méthodes populaires pour stocker : big endian et little endian. Le big endian est la méthode qui semble la plus naturelle. L'octet le plus fort (i.e. le plus significatif) est stocké en premier, puis le second plus fort, etc. Par exemple, le dword 00000004 serait stocké sous la forme des quatre octets suivants : 00 00 00 04.

Les mainframes IBM, la plupart des processeurs RISC et les processeurs Motorola utilisent tous cette méthode big endian. Cependant, les processeurs de type Intel utilisent la méthode little endian ! Ici, l'octet le moins significatif est stocké en premier. Donc, 00000004 est stocké en mémoire sous la forme 04 00 00 00. Ce format est codé en dur dans le processeur et ne peut pas être changé. Normalement, le programmeur n'a pas besoin de s'inquiéter du format utilisé. Cependant, il y a des circonstances où c'est important.

1. Lorsque des données binaires sont transférées entre différents ordinateurs (soit via des fichiers, soit via un réseau).

2. Lorsque des données binaires sont écrites en mémoire comme un entier multioctet puis relues comme des octets individuels ou vice versa.

Le caractère big ou little endian ne s'applique pas à l'ordre des éléments d'un tableau. Le premier élément d'un tableau est toujours à l'adresse la plus petite. C'est également valable pour les chaînes (qui sont juste des tableaux de caractères). Cependant, le caractère big ou little endian s'applique toujours aux éléments individuels des tableaux.

II-E. Fichier squelette

La Figure 1.7 montre un fichier squelette qui peut être utilisé comme point de départ pour l'écriture de programmes assembleur.

Image non disponible

III. Bases du langage assembleur

III-A. Travailler avec les entiers

III-A-1. Représentation des entiers

Les entiers se décomposent en deux catégories : signés et non signés. Les entiers non signés (qui sont positifs) sont représentés d'une manière binaire très intuitive. Le nombre 200 en tant qu'entier non signé sur un octet serait représenté par 11001000 (ou C8 en hexa).

Les entiers signés (qui peuvent être positifs ou négatifs) sont représentés d'une façon plus compliquée. Par exemple, considérons −56. +56 serait représenté par l'octet 00111000. Sur papier, on peut représenter −56 comme −111000, mais comment cela serait-il représenté dans un octet en mémoire de l'ordinateur. Comment serait stocké le signe moins ?

Il y a trois techniques principales qui ont été utilisées pour représenter les entiers signés dans la mémoire de l'ordinateur. Toutes ces méthodes utilisent le bit le plus significatif de l'entier comme bit de signe. Ce bit vaut 0 si le nombre est positif et 1 s'il est négatif.

III-A-1-a. Grandeur signée

La première méthode est la plus simple, elle est appelée grandeur signée.

Elle représente l'entier en deux parties. La première partie est le bit de signe et la seconde est la grandeur entière. Donc 56 serait représenté par l'octet 00111000 (le bit de signe est souligné) et −56 serait 10111000. La valeur d'octet la plus grande serait 01111111 soit +127 et la plus petite valeur sur un octet serait 11111111 soit −127. Pour obtenir l'opposé d'une valeur, on inverse le bit de signe. Cette méthode est intuitive, mais a ses inconvénients. Tout d'abord, il y a deux valeurs possibles pour 0 : +0 (00000000) et −0 (10000000). Comme zéro n'est ni positif ni négatif, ces deux représentations devraient se comporter de la même façon. Cela complique la logique de l'arithmétique pour le processeur. De plus, l'arithmétique générale est également compliquée. Si l'on ajoute 10 à −56, cela doit être transformé en 10 moins 56. Là encore, cela complique la logique du processeur.

III-A-1-b. Complément à 1

La seconde méthode est appelée représentation en complément à un.

Le complément à un d'un nombre est trouvé en inversant chaque bit du nombre (une autre façon de l'obtenir est que la valeur du nouveau bit est 1 − anciennevaleurdubit). Par exemple, le complément à un de 00111000 (+56) est 11000111. Dans la notation en complément à un, calculer le complément à un est équivalent à la négation. Donc 11000111 est la représentation de −56. Notez que le bit de signe a été automatiquement changé par le complément à un et que, comme l'on s'y attendait, appliquer le complément à un deux fois redonne le nombre de départ. Comme pour la première méthode, il y a deux représentations pour 0 : 00000000 (+0) et 11111111 (−0).

L'arithmétique des nombres en complément à 1 est compliquée. Voici une astuce utile pour trouver le complément à 1 d'un nombre en hexadécimal sans repasser en binaire. L'astuce est d'ôter le chiffre hexa de F (ou 15 en décimal). Cette méthode suppose que le nombre de bits dans le nombre est multiple de 4. Voici un exemple : +56 est représenté par 38 en hexa. Pour trouver le complément à 1, ôtez chaque chiffre de F pour obtenir C7 en hexa. Cela concorde avec le résultat ci-dessus.

III-A-1-c. Complément à 2

Les deux premières méthodes décrites ont été utilisées sur les premiers ordinateurs. Les ordinateurs modernes utilisent une troisième méthode appelée représentation en complément à 2. On trouve le complément à 2 d'un nombre en effectuant les deux opérations suivantes :

1. Trouver le complément à un du nombre ;

2. Ajouter 1 au résultat de l'étape 1.

Voici un exemple en utilisant 00111000 (56). Tout d'abord, on calcule le complément à 1 : 11000111. Puis, on ajoute 1 :

11000111

+ 1

11001000

Dans la notation en complément à 2, calculer le complément à 2 est équivalent à trouver l'opposé d'un nombre. Donc, 11001000 est la représentation en complément à 2 de −56. Deux négations devraient reproduire le nombre original. Curieusement le complément à 2 ne remplit pas cette condition. Prenez le complément à 2 de 11001000 en ajoutant 1 au complément à 1.

00110111

+ 1

00111000

Lorsque l'on effectue l'addition dans l'opération de complémentation à 2, l'addition du bit le plus à gauche peut produire une retenue. Cette retenue n'est pas utilisée. Souvenez-vous que toutes les données sur un ordinateur sont d'une taille fixe (en termes de nombre de bits). Ajouter deux octets produit toujours un résultat sur un octet (comme ajouter deux mots donne un mot, etc.) Cette propriété est importante pour la notation en complément à 2. Par exemple, considérons zéro comme un nombre en complément à 2 sur un octet (00000000). Calculer son complément à 2 produit la somme :

11111111

+ 1

c 00000000

où c représente une retenue (plus tard, nous montrerons comment détecter cette retenue, mais elle n'est pas stockée dans le résultat). Donc, en notation en complément à 2, il n'y a qu'un zéro. Cela rend l'arithmétique en complément à 2 plus simple que les méthodes précédentes.

En utilisant la notation en complément à 2, un octet signé peut être utilisé pour représenter les nombres −128 à +127.

Image non disponible

Le Tableau 2.1 montre quelques valeurs choisies. Si 16 bits sont utilisés, les nombres signés −32 768 à +32 767 peuvent être représentés. +32 767 est représenté par 7FFF, −32 768

par 8000, -128 par FF80 et -1 par FFFF. Les nombres en complément à 2 sur 32 bits vont de −2 milliards à +2 milliards environ.

Le processeur n'a aucune idée de ce qu'un octet en particulier (ou un mot ou un double-mot) est supposé représenté. L'assembleur n'a pas le concept de types qu'un langage de plus haut niveau peut avoir. La façon dont les données sont interprétées dépend de l'instruction dans laquelle on les utilise. Que la valeur FF soit considérée comme représentant un −1 signé ou 1 +255 non signé dépend du programmeur. Le langage C définit des types entiers signés et non signés. Cela permet au compilateur C de déterminer les instructions correctes à utiliser avec les données.

III-A-2. Extension de signe

En assembleur, toutes les données ont une taille bien spécifiée. Il n'est pas rare de devoir changer la taille d'une donnée pour l'utiliser avec d'autres. Réduire la taille est le plus simple.

III-A-2-a. Réduire la taille des données

Pour réduire la taille d'une donnée, il suffit d'en retirer les bits les plus significatifs. Voici un exemple trivial :

Sélectionnez
mov ax, 0034h ; ax = 52 (stocké sur 16 bits)
mov cl, al ; cl = les 8 bits de poids faible de ax

Bien sûr, si le nombre ne peut pas être représenté correctement dans une plus petite taille, réduire la taille ne fonctionne pas. Par exemple, si AX valait 0134h (ou 308 en décimal) alors le code ci-dessus mettrait quand même CL à 34h. Cette méthode fonctionne à la fois avec les nombres signés et non signés. Considérons les nombres signés, si AX valait FFFFh (−1 sur un mot), alors CL vaudrait FFh (−1 sur un octet). Cependant, notez que si la valeur dans AX était signée, elle aurait été tronquée et le résultat aurait été faux !

La règle pour les nombres non signés est que tous les bits retirés soient à 0 afin que la conversion soit correcte. La règle pour les nombres signés est que les bits retirés doivent soit tous être des 1 soit tous des 0. De plus, le premier bit à ne pas être retiré doit valoir la même chose que ceux qui l'ont été. Ce bit sera le bit de signe pour la valeur plus petite. Il est important qu'il ait la même valeur que le bit de signe original !

III-A-2-b. Augmenter la taille des données

Augmenter la taille des données est plus compliqué que la réduire. Considérons l'octet hexa FF. S'il est étendu à un mot, quelle valeur aura-t-il ? Cela dépend de la façon dont FF est interprété. Si FF est un octet non signé (255 en décimal), alors le mot doit être 00FF ; cependant, s'il s'agit d'un octet signé (−1 en décimal), alors le mot doit être FFFF.

En général, pour étendre un nombre non signé, on met tous les bits supplémentaires du nouveau nombre à 0. Donc, FF devient 00FF. Cependant, pour étendre un nombre signé, on doit étendre le bit de signe. Cela signifie que les nouveaux bits deviennent des copies du bit de signe. Comme le bit de signe de FF est à 1, les nouveaux bits doivent également être des 1, pour donner FFFF. Si le nombre signé 5A (90 en décimal) était étendu, le résultat serait 005A.

Il y a plusieurs instructions fournies par le 80386 pour étendre les nombres.

Souvenez-vous que l'ordinateur ne sait pas si un nombre est signé ou non. C'est au programmeur d'utiliser l'instruction adéquate. Pour les nombres non signés, on peut placer des 0 dans les bits de poids fort de façon simple en utilisant l'instruction MOV. Par exemple pour étendre l'octet de AL en un mot non signé dans AX :

Sélectionnez
mov ah, 0 ; met les 8 bits de poids fort à 0

Cependant, il n'est pas possible d'utiliser une instruction MOV pour convertir le mot non signé de AX en un double-mot non signé dans EAX. Pourquoi pas ? Il n'y a aucune façon d'accéder aux 16 bits de poids fort de EAX dans un MOV. Le 80386 résout ce problème en fournissant une nouvelle instruction : MOVZX. Cette instruction a deux opérandes. La destination (premier opérande) doit être un registre de 16 ou 32 bits. La source (deuxième opérande) doit être un registre 8 ou 16 bits. L'autre restriction est que la destination doit être plus grande que la source (la plupart des instructions requièrent que la source soit de la même taille que la destination). Voici quelques exemples :

Sélectionnez
movzx eax, ax ; étend ax à eax

movzx eax, al ; étend al à eax

movzx ax, al ; étend al à ax

movzx ebx, ax ; étend ax à ebx

Pour les nombres signés, il n'y a pas de façon simple d'utiliser l'instruction MOV quel que soit le cas. Le 8086 fournit plusieurs instructions pour étendre les nombres signés. L'instruction CBW (Convert Byte to Word, Convertir un Octet en Mot) étend le signe du registre AL dans AX. Les opérandes sont implicites. L'instruction CWD (Convert Word to Double word, Convertir un Mot en Double-mot) étend le signe de AX dans DX:AX. La notation DX:AX signifie qu'il faut considérer les registres DX et AX comme un seul registre 32 bits avec les 16 bits de poids forts dans DX et les bits de poids faible dans AX (souvenez-vous que le 8086 n'avait pas de registre 32 bits du tout !). Le 80386 a apporté plusieurs nouvelles instructions. L'instruction CWDE (Convert Word to Double word Extended, convertir un mot en double mot étendue) étend le signe de AX dans EAX. L'instruction CDQ (Convert Double word to Quad word, Convertir un Double-mot en Quadruple-mot) étend le signe de EAX dans EDX:EAX (64 bits !) Enn, l'instruction MOVSX fonctionne comme MOVZX excepté qu'elle utilise les règles des nombres signés.

III-A-2-c. Application à la programmation en C

L'extension d'entiers signés et non signés a également lieu en C. Les variables en C peuvent être déclarées soit comme signées soit comme non signées (les int sont signés).

Image non disponible

Le C ANSI ne définit pas si le type char est signé ou non, c'est à chaque compilateur de le décider. C'est pourquoi on définit le type explicitement dans la Figure 2.1.

Considérons le code de la Figure 2.1. À la ligne 3, la variable a est étendue en utilisant les règles pour les valeurs non signées (avec MOVZX), mais à la ligne 4, les règles signées sont utilisées pour b (avec MOVSX).

Il existe un bogue de programmation courant en C qui est directement lié à notre sujet. Considérons le code de la Figure 2.2. Le prototype de fgetc()est :

Sélectionnez
int fgetc( FILE * );

On peut se demander pourquoi la fonction renvoie un int puisqu'elle lit des caractères. La raison est qu'elle renvoie normalement un char (étendu à une valeur int en utilisant l'extension de zéro). Cependant, il y a une valeur qu'elle peut retourner qui n'est pas un caractère, EOF. C'est une macro habituellement définie comme valant −1. Donc, fgetc() retourne soit un char étendu à une valeur int (qui ressemble à 000000xx en hexa) soit EOF (qui ressemble à FFFFFFFF en hexa).

Le problème avec le programme de la Figure 2.2 est que fgetc() renvoie un int, mais cette valeur est stockée dans un char. Le C tronquera alors les bits de poids fort pour faire tenir la valeur de l'int dans le char. Le seul problème est que les nombres (en hexa) 000000FF et FFFFFFFF seront tous deux tronqués pour donner l'octet FF. Donc, la boucle while ne peut pas distinguer la lecture de l'octet FF dans le fichier et la fin de ce fichier.

Ce que fait exactement le code dans ce cas change selon que le char est signé ou non. Pourquoi ? Parce que ligne 2, ch est comparé avec EOF. Comme EOF est un int(11), ch sera étendu à un int afin que les deux valeurs comparées soient de la même taille(12). Comme la Figure 2.1 le montrait, le fait qu'une variable soit signée ou non est très important.

Si le char n'est pas signé, FF est étendu et donne 000000FF. Cette valeur est comparée à EOF (FFFFFFFF) et est différente. Donc la boucle ne finit jamais !

Si le char est signé, FF est étendu et donne FFFFFFFF. Les deux valeurs sont alors égales et la boucle se termine. Cependant, comme l'octet FF peut avoir été lu depuis le fichier, la boucle peut se terminer prématurément.

La solution à ce problème est de définir la variable ch comme un int, pas un char. Dans ce cas, aucune troncature ou extension n'est effectuée à la ligne 2. Dans la boucle, il est sans danger de tronquer la valeur puisque ch doit alors être réellement un octet.

III-A-3. Arithmétique en complément à 2

Comme nous l'avons vu plus tôt, l'instruction add effectue une addition et l'instruction sub effectue une soustraction. Deux des bits du registre EFLAGS que ces instructions positionnent sont overow (dépassement de capacité) et carry ag (retenue). Le drapeau d'overow est à 1 si le résultat réel de l'opération est trop grand et ne tient pas dans la destination pour l'arithmétique signée. Le drapeau de retenue est à 1 s'il y a une retenue dans le bit le plus significatif d'une addition d'une soustraction. Donc, il peut être utilisé pour détecter un dépassement de capacité en arithmétique non signée. L'utilisation du drapeau de retenue pour l'arithmétique signée va être vue sous peu. Un des grands avantages du complément à 2 est que les règles pour l'addition et la soustraction sont exactement les mêmes que pour les entiers non signés. Donc, add et sub peuvent être utilisées pour les entiers signés ou non.

002C 44

+ FFFF + (−1)

002B 43

Il y a une retenue de générée, mais elle ne fait pas partie de la réponse.

Il y a deux instructions de multiplication et de division différentes. Tout d'abord, pour multiplier, utilisez les instructions MUL ou IMUL. L'instruction MUL est utilisée pour multiplier les nombres non signés et IMUL est utilisée pour multiplier les entiers signés. Pourquoi deux instructions différentes sont nécessaires ? Les règles pour la multiplication sont différentes pour les nombres non signés et les nombres signés en complément à 2. Pourquoi cela ?

Image non disponible

Considérons la multiplication de l'octet FF avec lui-même donnant un résultat sur un mot. En utilisant la multiplication non signée, il s'agit de 255 fois 255 soit 65 025 (ou FE01 en hexa). En utilisant la multiplication signée, il s'agit de −1 fois −1 soit 1 (ou 0001 en hexa).

Il y a plusieurs formes pour les instructions de multiplication. La plus ancienne ressemble à cela :

Sélectionnez
mul source

La source est soit un registre soit une référence mémoire. Cela ne peut pas être une valeur immédiate. Le type exact de la multiplication dépend de la taille de l'opérande source. Si l'opérande est un octet, elle est multipliée par l'octet situé dans le registre AL et le résultat est stocké dans les 16 bits de AX. Si la source fait 16 bits, elle est multipliée par le mot situé dans AX et le résultat 32 bits est stocké dans DX:AX. Si la source fait 32 bits, elle est multipliée par EAX et le résultat 64 bits est stocké dans EDX:EAX.

L'instruction IMUL a les mêmes formats que MUL, mais ajoute également d'autres formats d'instruction. Il y a des formats à deux et à trois opérandes :

Sélectionnez
imul dest, source1

imul dest, source1, source2

Le Tableau 2.2 montre les combinaisons possibles.

Les deux opérateurs de division sont DIV et IDIV. Ils effectuent respectivement une division entière non signée et une division entière signée. Le format général est :

Sélectionnez
div source

Si la source est sur 8 bits, alors AX est divisé par l'opérande. Le quotient est stocké dans AL et le reste dans AH. Si la source est sur 16 bits, alors DX:AX est divisé par l'opérande. Le quotient est stocké dans AX et le reste dans DX. Si la source est sur 32 bits, alors EDX:EAX est divisé par l'opérande, le quotient est stocké dans EAX et le reste dans EDX. L'instruction IDIV fonctionne de la même façon. Il n'y a pas d'instructions IDIV spéciales comme pour IMUL. Si le quotient est trop grand pour tenir dans son registre ou que le diviseur vaut 0, le programme et interrompu et se termine. Une erreur courante est d'oublier d'initialiser DX ou EDX avant la division.

L'instruction NEG inverse son opérande en calculant son complément à 2. L'opérande peut être n'importe quel registre ou emplacement mémoire sur 8 bits, 16 bits, or 32 bits.

III-A-4. Programme exemple

math.asm
Sélectionnez
1 %include "asm_io.inc"
2 segment .data ; Chaines affichees
3 prompt db "Entrez un nombre : ", 0
4 square_msg db "Le carre de l'entree vaut ", 0
5 cube_msg db "Le cube de l'entree vaut ", 0
6 cube25_msg db "Le cube de l'entree fois 25 vaut ", 0
7 quot_msg db "Le quotient de cube/100 vaut ", 0
8 rem_msg db "Le reste de cube/100 vaut ", 0
9 neg_msg db "La négation du reste vaut ", 0
10
11 segment .bss
12 input resd 1
13
14 segment .text
15 global _asm_main
16 _asm_main :
17 enter 0,0 ; routine d'initialisation
18 pusha
19
20 mov eax, prompt
21 call print_string
22
23 call read_int
24 mov [input], eax
25
26 imul eax ; edx:eax = eax * eax
27 mov ebx, eax ; sauvegarde le résultat dans ebx
28 mov eax, square_msg
29 call print_string
30 mov eax, ebx
31 call print_int
32 call print_nl
33
34 mov ebx, eax
35 imul ebx, [input] ; ebx *= [entree]
36 mov eax, cube_msg
37 call print_string
38 mov eax, ebx
39 call print_int
40 call print_nl
41
42 imul ecx, ebx, 25 ; ecx = ebx*25
43 mov eax, cube25_msg
44 call print_string
45 mov eax, ecx
46 call print_int
47 call print_nl
48
49 mov eax, ebx
50 cdq ; initialise edx avec extension de signe
51 mov ecx, 100 ; on ne peut pas diviser par une valeur immédiate
52 idiv ecx ; edx:eax / ecx
53 mov ecx, eax ; sauvegarde le résultat dans ecx
54 mov eax, quot_msg
55 call print_string
56 mov eax, ecx
57 call print_int
58 call print_nl
59 mov eax, rem_msg
60 call print_string
61 mov eax, edx
62 call print_int
63 call print_nl
64
65 neg edx ; inverse le reste
66 mov eax, neg_msg
67 call print_string
68 mov eax, edx
69 call print_int
70 call print_nl
71
72 popa
73 mov eax, 0 ; retour au C
74 leave
75 ret

III-A-5. Arithmétique en précision étendue

Le langage assembleur fournit également des instructions qui permettent d'effectuer des additions et des soustractions sur des nombres plus grands que des doubles mots. Ces instructions utilisent le drapeau de retenue. Comme nous l'avons dit plus haut, les instructions ADD et SUB modifient le drapeau de retenue si une retenue est générée. Cette information stockée dans le drapeau de retenue peut être utilisée pour additionner ou soustraire de grands nombres en morcelant l'opération en doubles-mots (ou plus petit).

Les instructions ADC et SBB utilisent les informations données par le drapeau de retenue. L'instruction ADC effectue l'opération suivante :

opérande1 = opérande1 + drapeau de retenue + opérande2

L'instruction SBB effectue :

opérande1 = opérande1 - drapeau de retenue - opérande2

Comment sont-elles utilisées ? Considérons la somme d'entiers 64 bits dans EDX:EAX et EBX:ECX. Le code suivant stockerait la somme dans EDX:EAX :

Sélectionnez
1 add eax, ecx ; additionne les 32 bits de poids faible

2 adc edx, ebx ; additionne les 32 bits de poids fort et la retenue

La soustraction est très similaire. Le code suivant soustrait EBX:ECX de EDX:EAX :

Sélectionnez
1 sub eax, ecx ; soustrait les 32 bits de poids faible

2 sbb edx, ebx ; soustrait les 32 bits de poids fort et la retenue

Pour les nombres vraiment grands, une boucle peut être utilisée (voir Section III.B). Pour une boucle de somme, il serait pratique d'utiliser l'instruction ADC pour toutes les itérations (au lieu de toutes sauf la première).

Cela peut être fait en utilisant l'instruction CLC (CLear Carry) juste avant que la boucle ne commence pour initialiser le drapeau de retenue à 0. Si le drapeau de retenue vaut 0, il n'y a pas de différence entre les instructions ADD et ADC. La même idée peut être utilisée pour la soustraction.

III-B. Structures de contrôle

Les langages de haut niveau fournissent des structures de contrôle de haut niveau (p.e., les instructions if et while) qui contrôlent le flot d'exécution. Le langage assembleur ne fournit pas de structures de contrôle aussi complexes. Il utilise à la place l'infâme goto et l'utiliser de manière inappropriée peut conduire à du code spaghetti ! Cependant, il est possible d'écrire des programmes structurés en assembleur. La procédure de base est de concevoir la logique du programme en utilisant les structures de contrôle de haut niveau habituelles et de traduire cette conception en langage assembleur (comme le ferait un compilateur).

III-B-1. Comparaison

Les structures de contrôle décident ce qu'il faut faire en comparant des données. En assembleur, le résultat d'une comparaison est stocké dans le registre FLAGS pour être utilisé plus tard. Le 80x86 fournit l'instruction CMP pour effectuer des comparaisons. Le registre FLAGS est positionné selon la différence entre les deux opérandes de l'instruction CMP. Les opérandes sont soustraits et le registre FLAGS est positionné selon le résultat, mais ce résultat n'est stocké nulle part. Si vous avez besoin du résultat, utilisez l'instruction SUB au lieu de CMP.

Pour les entiers non signés, il y a deux drapeaux (bits dans le registre FLAGS) qui sont importants : les drapeaux zéro (ZF) et retenue (CF). Le drapeau zero est allumé (1) si le résultat de la différence serait 0. Considérons une comparaison comme :

Sélectionnez
cmp vleft, vright

La différence vleft - vright est calculée et les drapeaux sont positionnés en fonction. Si la différence calculée par CMP vaut 0, vleft = vright, alors ZF est allumé(i.e. 1) et CF est éteint (i.e. 0). Si vleft > vright, ZF est éteint et CF également. Si vleft < vright, alors ZF est éteint et CF est allumé (retenue).

Pour les entiers signés, il y a trois drapeaux importants : le drapeau zéro (ZF), le drapeau d'overow (OF) et le drapeau de signe (SF). Le drapeau d'overow est allumé si le résultat d'une opération dépasse la capacité (de trop ou de trop peu). Le drapeau de signe est allumé si le résultat d'une opération est négatif. Si vleft = vright, ZF est allumé (comme pour les entiers non signés). Si vleft > vright, ZF est éteint et SF = OF. Si vleft < vright, ZF est éteint et SF 6= OF.

N'oubliez pas que d'autres instructions peuvent aussi changer le registre FLAGS, pas seulement CMP.

Pourquoi SF = OF si vleft > vright ? S'il n'y
a pas d'overow, alors la différence aura la valeur correcte et doit être positive ou nulle. Donc, SF = OF = 0. Par contre, s'il y a un overow, la différence n'aura pas la valeur correcte (et sera en fait négative).
Donc, SF = OF = 1.

III-B-2. Instructions de branchement

Les instructions de branchement peuvent transférer l'exécution à n'importe quel point du programme. En d'autres termes, elles agissent comme un goto. Il y a deux types de branchements : conditionnel et inconditionnel. Un branchement inconditionnel est exactement comme un goto, il effectue toujours le branchement. Un branchement conditionnel peut effectuer le branchement ou non selon les drapeaux du registre FLAGS. Si un branchement conditionnel n'effectue pas le branchement, le contrôle passe à l'instruction suivante.

L'instruction JMP (abréviation de jump) effectue les branchements inconditionnels.

Son seul argument est habituellement l'étiquette de code de l'instruction à laquelle se brancher. L'assembleur ou l'éditeur de liens remplacera l'étiquette par l'adresse correcte de l'instruction. C'est une autre des opérations pénibles que l'assembleur fait pour rendre la vie du programmeur plus simple. Il est important de réaliser que l'instruction immédiatement après l'instruction JMP ne sera jamais exécutée à moins qu'une autre instruction ne se branche dessus !

Il y a plusieurs variantes de l'instruction de saut :

SHORT ce saut est très limité en portée. Il ne peut bouger que de plus ou moins 128 octets en mémoire. L'avantage de ce type de saut est qu'il utilise moins de mémoire que les autres. Il utilise un seul octet signé pour stocker le déplacement du saut. Le déplacement est le nombre d'octets à sauter vers l'avant ou vers l'arrière (le déplacement est ajouté à EIP). Pour spécifier un saut court, utilisez le mot-clé SHORT immédiatement avant l'étiquette dans l'instruction JMP ;

NEAR ce saut est le saut par défaut, que ce soit pour les branchements inconditionnels ou conditionnels, il peut être utilisé pour sauter vers n'importe quel emplacement dans un segment. En fait, le 80386 supporte deux types de sauts proches. L'un utilise deux octets pour le déplacement. Cela permet de se déplacer en avant ou en arrière d'environ 32 000 octets. L'autre type utilise quatre octets pour le déplacement, ce qui, bien sûr, permet de se déplacer n'importe où dans le segment.

Le type de saut à deux octets peut être spécifié en plaçant le mot-clé WORD avant l'étiquette dans l'instruction JMP.

FAR ce saut permet de passer le contrôle à un autre segment de code. C'est une chose très rare en mode protégé 386.

Les étiquettes de code valides suivent les mêmes règles que les étiquettes de données. Les étiquettes de code sont définies en les plaçant dans le segment de code au début de l'instruction qu'ils étiquettent. Deux points sont placés à la fin de l'étiquette lors de sa définition. Ces deux points ne font pas partie du nom.

Image non disponible

Il y a beaucoup d'instructions de branchement conditionnel différentes. Elles prennent toutes une étiquette de code comme seul opérande. Les plus simples se contentent de regarder un drapeau dans le registre FLAGS pour déterminer si elles doivent sauter ou non. Voyez le Tableau 2.3 pour une liste de ces instructions (PF est le drapeau de parité qui indique si le nombre de bits dans les 8 bits de poids faible du résultat est pair ou impair).

Le pseudo-code suivant :

Sélectionnez
if ( EAX == 0 )

EBX = 1;

else

EBX = 2;

peut être écrit de cette façon en assembleur :

Sélectionnez
1 cmp eax, 0 ; positionne les drapeaux (ZF allumé si eax - 0 = 0)

2 jz thenblock ; si ZF est allumé, branchement vers thenblock

3 mov ebx, 2 ; partie ELSE du IF

4 jmp next ; saute par dessus la partie THEN du IF

5 thenblock :

6 mov ebx, 1 ; partie THEN du IF

7 next :

Les autres comparaisons ne sont pas si simples si l'on se contente des branchements conditionnels du Tableau 2.3. Pour illustrer, considérons le pseudo-code suivant :

Sélectionnez
if ( EAX >= 5 )

EBX = 1;

else

EBX = 2;

Si EAX est plus grand ou égal à 5, ZF peut être allumé ou non et SF sera égal à OF. Voici le code assembleur qui teste ces conditions (en supposant que EAX est signé) :

Sélectionnez
1 cmp eax, 5
2 js signon ; goto signon si SF = 1
3 jo elseblock ; goto elseblock si OF = 1 et SF = 0
4 jmp thenblock ; goto thenblock si SF = 0 et OF = 0
5 signon:
6 jo thenblock ; goto thenblock si SF = 1 et OF = 1
7 elseblock :
8 mov ebx, 2
9 jmp next
10 thenblock:
11 mov ebx, 1
12 next:

Le code ci-dessus est très confus. Heureusement, le 80x86 fournit des instructions de branchement additionnelles pour rendre ce type de test beaucoup plus simple. Il y a des versions signées et non signées de chacune.

Image non disponible

Le Tableau 2.4 montre ces instructions. Les branchements égal et non égal (JE et JNE) sont les mêmes pour les entiers signés et non signés (en fait, JE et JNE sont réellement identiques à JZ et JNZ, respectivement). Chacune des autres instructions de branchement a deux synonymes. Par exemple, regardez JL (jump less than, saut si inférieur à) et JNGE (jump not greater than or equal to, saut si non plus grand ou égal). Ce sont les mêmes instructions, car :

x < y => not(x y)

Les branchements non signés utilisent A pour above (au-dessus) et B pour below (en dessous) à la place de L et G.

En utilisant ces nouvelles instructions de branchement, le pseudo-code ci-dessus peut être traduit en assembleur plus facilement.

Sélectionnez
1 cmp eax, 5
2 jge thenblock
3 mov ebx, 2
4 jmp next
5 thenblock:
6 mov ebx, 1
7 next:

III-B-3. Les instructions de boucle

Le 80x86 fournit plusieurs instructions destinées à implémenter des boucles de style for. Chacune de ces instructions prend une étiquette de code comme seul opérande.

LOOP décrémente ECX, si ECX 6= 0, saute vers l'étiquette.

LOOPE, LOOPZ décrémente ECX (le registre FLAGS n'est pas modifié), si ECX 6= 0 et ZF = 1, saute.

LOOPNE, LOOPNZ décrémente ECX (FLAGS inchangé), si ECX 6= 0 et ZF = 0, saute.

Les deux dernières instructions de boucle sont utiles pour les boucles de recherche séquentielle. Le pseudo-code suivant :

Sélectionnez
sum = 0;

for ( i=10; i >0; i&#8722;&#8722; )

sum += i;

peut être traduit en assembleur de la façon suivante ;

Sélectionnez
1 mov eax, 0 ; eax est sum
2 mov ecx, 10 ; ecx est i
3 loop_start:
4 add eax, ecx
5 loop loop_start

III-C. Traduire les structures de contrôle standard

Cette section décrit comment les structures de contrôle standard des langages de haut niveau peuvent être implémentées en langage assembleur.

III-C-1. Instructions if

Le pseudo-code suivant :

Sélectionnez
if ( condition )
then_block;
else
else_block;

peut être implémenté de cette façon :

Sélectionnez
1 ; code pour positionner FLAGS
2 jxx else_block ; choisir xx afin de sauter si la condition est fausse
3 ; code pour le bloc then
4 jmp endif
5 else_block :
6 ; code pour le bloc else
7 endif :

S'il n'y a pas de else, le branchement else_block peut être remplacé par un branchement à endif.

Sélectionnez
1 ; code pour positionner FLAGS
2 jxx endif ; choisir xx afin de sauter si la condition est fausse
3 ; code pour le bloc then
4 endif :

III-C-2. Boucles while

La boucle while est une boucle avec un test au début :

Sélectionnez
while( condition ) {

corps de la boucle;

}

Cela peut être traduit en :

Sélectionnez
1 while :
2 ; code pour positionner FLAGS selon la condition
3 jxx endwhile ; choisir xx pour sauter si la condition est fausse
4 ; body of loop
5 jmp while
6 endwhile :

III-C-3. Boucles do while

La boucle do while est une boucle avec un test à la fin :

Sélectionnez
do {

corps de la boucle;

} while( condition );

Cela peut être traduit en :

Sélectionnez
1 do :

2 ; corps de la boucle

3 ; code pour positionner FLAGS selon la condition

4 jxx do ; choisir xx pour se brancher si la condition est vraie

III-D. Exemple : trouver des nombres premiers

Cette section détaille un programme qui trouve des nombres premiers.

Rappelez-vous que les nombres premiers ne sont divisibles que par 1 et par eux-mêmes. Il n'y a pas de formule pour les rechercher. La méthode de base qu'utilise ce programme est de rechercher les diviseurs de tous les nombres impairs(13) sous une limite donnée. Si aucun facteur ne peut être trouvé pour un nombre impair, il est premier. Voici l'algorithme de base écrit en C.

Sélectionnez
1 unsigned guess; / candidat courant pour être premier /

2 unsigned factor ; / diviseur possible de guess /

3 unsigned limit ; / rechercher les nombres premiers jusqu'à cette valeur /

4

5  printf ("Rechercher les nombres premiers jusqu'a : ");

6 scanf("%u", &limit);

7 printf ("2\n"); / traite les deux premiers nombres premiers comme /

8 printf ("3\n"); / des cas spéciaux /

9 guess = 5; / candidat initial /

10 while ( guess <= limit ) {

11 / recherche un diviseur du candidat /

12 factor = 3;

13 while ( factor factor < guess &&

14 guess % factor != 0 )

15 factor += 2;

16 if ( guess % factor != 0 )

17 printf ("%d\n", guess);

18 guess += 2; / ne regarder que les nombres impairs /

19 }

Voici la version assembleur :

prime.asm
Sélectionnez
1 %include "asm_io.inc"
2 segment .data
3 Message db "Rechercher les nombres premiers jusqu'a : ", 0
4
5  segment .bss
6 Limit resd 1 ; rechercher les nombres premiers jusqu'à cette limite
7 Guess resd 1 ; candidat courant pour être premier
8
9  segment .text
10 global _asm_main
11 _asm_main :
12 enter 0,0 ; routine d'initialisation
13 pusha
14
15 mov eax, Message
16 call print_string
17 call read_int ; scanf("%u", &limit);
18 mov [Limit], eax
19
20 mov eax, 2 ; printf("2\n");
21 call print_int
22 call print_nl
23 mov eax, 3 ; printf("3\n");
24 call print_int
25 call print_nl
26
27 mov dword [Guess], 5 ; Guess = 5;
28 while_limit : ; while ( Guess <= Limit )
29 mov eax,[Guess]
30 cmp eax, [Limit]
31 jnbe end_while_limit ; on utilise jnbe, car les nombres ne sont pas signes
32
33 mov ebx, 3 ; ebx est factor = 3;
34 while_factor:
35 mov eax,ebx
36 mul eax ; edx:eax = eax*eax
37 jo end_while_factor ; si la réponse ne tient pas dans eax seul
38 cmp eax, [Guess]
39 jnb end_while_factor ; si !(factor*factor < guess)
40 mov eax,[Guess]
41 mov edx,0
42 div ebx ; edx = edx:eax % ebx
43 cmp edx, 0
44 je end_while_factor ; si !(guess % factor != 0)
45
46 add ebx,2 ; factor += 2;
47 jmp while_factor
48 end_while_factor:
49 je end_if ; si !(guess % factor != 0)
50 mov eax,[Guess] ; printf("%u\n")
51 call print_int
52 call print_nl
53 end_if :
54 add dword [Guess], 2 ; guess += 2
55 jmp while_limit
56 end_while_limit:
57
58 popa
59 mov eax, 0 ; retour au C
60 leave
61 ret

IV. Opérations sur les bits

IV-A. Opérations de décalage

Le langage assembleur permet au programmeur de manipuler individuellement les bits des données. Une opération courante sur les bits est appelée décalage. Une opération de décalage déplace la position des bits d'une donnée. Les décalages peuvent être soit vers la gauche (i.e. vers les bits les plus significatifs) soit vers la droite (les bits les moins significatifs).

IV-A-1. Décalages logiques

Le décalage logique est le type le plus simple de décalage. Il décale d'une manière très simple. La Figure 3.1 montre l'exemple du décalage d'un nombre sur un octet.

Image non disponible

Notez que les nouveaux bits sont toujours à 0. Les instructions SHL et SHR sont respectivement utilisées pour décaler à gauche et à droite. Ces instructions permettent de décaler de n'importe quel nombre de positions.

Le nombre de positions à décaler peut soit être une constante, soit être stocké dans le registre CL. Le dernier bit décalé de la donnée est stocké dans le drapeau de retenue. Voici quelques exemples :

Sélectionnez
1 mov ax, 0C123H

2 shl ax, 1 ; décale d'1 bit à gauche, ax = 8246H, CF = 1

3 shr ax, 1 ; décale d'1 bit à droite, ax = 4123H, CF = 0

4 shr ax, 1 ; décale d'1 bit à droite, ax = 2091H, CF = 1

5 mov ax, 0C123H

6 shl ax, 2 ; décale de 2 bits à gauche, ax = 048CH, CF = 1

7 mov cl, 3

8 shr ax, cl ; décale de 3 bits à droite, ax = 0091H, CF = 1

IV-A-2. Utilisation des décalages

La multiplication et la division rapides sont les utilisations les plus courantes des opérations de décalage. Rappelez-vous que dans le système décimal, la multiplication et la division par une puissance de dix sont simples, on décale simplement les chiffres. C'est également vrai pour les puissances de deux en binaire. Par exemple, pour multiplier par deux le nombre binaire 10112 (ou 11 en décimal), décalez d'un cran vers la gauche pour obtenir 101102 (ou 22). Le quotient d'une division par une puissance de 2 est le résultat d'un décalage à droite. Pour diviser par 2, utilisez un décalage d'une position à droite ; pour diviser par 4 (22), décalez à droite de deux positions ; pour diviser par 8 (23), décalez de 3 positions vers la droite, etc. Les instructions de décalage sont très basiques et sont beaucoup plus rapides que les instructions MUL et DIV correspondantes !

En fait, les décalages logiques peuvent être utilisés pour multiplier ou diviser des valeurs non signées. Ils ne fonctionnent généralement pas pour les valeurs signées. Considérons la valeur sur deux octets FFFF (−1 signé). Si on lui applique un décalage logique d'une position vers la droite, le résultat est 7FFF ce qui fait +32 767 ! Un autre type de décalage doit être utilisé pour les valeurs signées.

IV-A-3. Décalages arithmétiques

Ces décalages sont conçus pour permettre à des nombres signés d'être rapidement multipliés et divisés par des puissances de 2. Ils assurent que le bit de signe est traité correctement.

SAL Shift Arithmetic Left (décalage arithmétique à gauche) - Cette instruction est juste un synonyme pour SHL. Elle est assemblée pour donner exactement le même code machine que SHL. Tant que le bit de signe n'est pas changé par le décalage, le résultat sera correct.

SAR Shift Arithmetic Right (décalage arithmétique à droite) - C'est une nouvelle instruction qui ne décale pas le bit de signe (i.e. le msb) de son opérande. Les autres bits sont décalés normalement sauf que les nouveaux bits qui entrent par la gauche sont des copies du bit de signe (c'est-à-dire que si le bit de signe est à 1, les nouveaux bits sont également à 1). Donc, si un octet est décalé avec cette instruction, seuls les 7 bits de poids faible sont décalés. Comme pour les autres décalages, le dernier bit sorti est stocké dans le drapeau de retenue.

Sélectionnez
1 mov ax, 0C123H

2 sal ax, 1 ; ax = 8246H, CF = 1

3 sal ax, 1 ; ax = 048CH, CF = 1

4 sar ax, 2 ; ax = 0123H, CF = 0

IV-A-4. Décalages circulaires

Les instructions de décalage circulaire fonctionnent comme les décalages logiques excepté que les bits perdus à un bout sont réintégrés à l'autre. Donc, la donnée est traitée comme s'il s'agissait d'une structure circulaire. Les deux instructions de rotation les plus simples sont ROL et ROR qui effectuent des rotations à gauche et à droite, respectivement. Comme pour les autres décalages, ceux-ci laissent une copie du dernier bit sorti dans le drapeau de retenue.

Sélectionnez
1 mov ax, 0C123H

2 rol ax, 1 ; ax = 8247H, CF = 1

3 rol ax, 1 ; ax = 048FH, CF = 1

4 rol ax, 1 ; ax = 091EH, CF = 0

5 ror ax, 2 ; ax = 8247H, CF = 1

6 ror ax, 1 ; ax = C123H, CF = 1

Il y a deux instructions de rotation supplémentaires qui décalent les bits de la donnée et le drapeau de retenue appelés RCL et RCR. Par exemple, si on applique au registre AX une rotation avec ces instructions, elle est appliquée aux 17 bits constitués de AX et du drapeau de retenue.

Sélectionnez
1 mov ax, 0C123H

2 clc ; eteint le drapeau de retenue (CF = 0)

3 rcl ax, 1 ; ax = 8246H, CF = 1

4 rcl ax, 1 ; ax = 048DH, CF = 1

5 rcl ax, 1 ; ax = 091BH, CF = 0

6 rcr ax, 2 ; ax = 8246H, CF = 1

7 rcr ax, 1 ; ax = C123H, CF = 0

IV-A-5. Application simple

Voici un extrait de code qui compte le nombre de bits qui sont allumés (i.e. à 1) dans le registre EAX.

Sélectionnez
1 mov bl, 0 ; bl contiendra le nombre de bits ALLUMES
2 mov ecx, 32 ; ecx est le compteur de boucle
3 count_loop:
4 shl eax, 1 ; decale un bit dans le drapeau de retenue
5 jnc skip_inc ; si CF == 0, goto skip_inc
6 inc bl
7 skip_inc :
8 loop count_loop

Le code ci-dessus détruit la valeur originale de EAX (EAX vaut zéro à la fin de la boucle). Si l'on voulait conserver la valeur de EAX, la ligne 4 pourrait être remplacée par rol eax, 1.

IV-B. Opérations booléennes niveau bit

Image non disponible

Il y a quatre opérateurs booléens courants : AND, OR, XOR et NOT. Une table de vérité montre le résultat de chaque opération pour chaque valeur possible de ses opérandes.

IV-B-1. L'opération ET

Le résultat d'un ET sur deux bits vaut 1 uniquement si les deux bits sont à 1, sinon, le résultat vaut 0, comme le montre la table de vérité du Tableau 3.1.

Les processeurs supportent ces opérations comme des instructions agissant de façon indépendante sur tous les bits de la donnée en parallèle. Par exemple, si on applique un ET au contenu de AL et BL, l'opération ET de base est appliquée à chacune des 8 paires de bits correspondantes dans les deux registres, comme le montre la Figure 3.2. Voici un exemple de code :

Sélectionnez
1 mov ax, 0C123H

2 and ax, 82F6H ; ax = 8022H

IV-B-2. L'opération OU

Image non disponible

Le OU de 2 bits vaut 0 uniquement si les deux bits valent 0, sinon le résultat vaut 1 comme le montre la table de vérité du Tableau 3.2. Voici un exemple de code :

Sélectionnez
1 mov ax, 0C123H
2 or ax, 0E831H ; ax = E933H

IV-B-3. L'opération XOR

Le OU exclusif de deux bits vaut 0 uniquement si les deux bits sont égaux, sinon, le résultat vaut 1 comme le montre la table de vérité du Tableau 3.3.

Voici un exemple de code :

Sélectionnez
1 mov ax, 0C123H

2 xor ax, 0E831H ; ax = 2912H

IV-B-4. L'opération NOT

Image non disponible

L'opération NOT est une opération unaire (i.e. elle agit sur un seul opérande, pas deux comme les opérations binaires de type ET). Le NOT d'un bit est la valeur opposée du bit comme le montre la table de vérité du Tableau 3.4. Voici un exemple de code :

Sélectionnez
1 mov ax, 0C123H

2 not ax ; ax = 3EDCH

Notez que le NOT donne le complément à un. Contrairement aux autres opérations niveau bit, l'instruction NOT ne change aucun des bits du registre FLAGS.

IV-B-5. L'instruction TEST

L'instruction TEST effectue une opération AND, mais ne stocke pas le résultat. Elle positionne le registre FLAGS selon ce que ce dernier aurait été après un AND (comme l'instruction CMP qui effectue une soustraction, mais ne fait que positionner FLAGS). Par exemple, si le résultat était zéro, ZF serait allumé.

IV-B-6. Utilisation des opérations sur les bits

Les opérations sur les bits sont très utiles pour manipuler individuellement les bits d'une donnée sans modifier les autres bits. Le Tableau 3.5 montre trois utilisations courantes de ces opérations.

Image non disponible

Voici un exemple de code, implémentant ces idées.

Sélectionnez
1 mov ax, 0C123H

2 or ax, 8 ; allumer le bit 3, ax = C12BH

3 and ax, 0FFDFH ; éteindre le bit 5, ax = C10BH

4 xor ax, 8000H ; inverser le bit 31, ax = 410BH

5 or ax, 0F00H ; allumer un quadruplet, ax = 4F0BH

6 and ax, 0FFF0H ; éteindre un quadruplet, ax = 4F00H

7 xor ax, 0F00FH ; inverser des quadruplets, ax = BF0FH

8 xor ax, 0FFFFH ; complément à 1, ax = 40F0H

L'opération ET peut aussi être utilisée pour trouver le reste d'une division par une puissance de deux. Pour trouver le reste d'une division par 2i, effectuez un ET sur le nombre avec un masque valant 2i − 1. Ce masque contiendra des 1 du bit 0 au bit i−1. Ce sont tout simplement ces bits qui correspondent au reste. Le résultat du ET conservera ces bits et mettra les autres à zéro. Voici un extrait de code qui trouve le quotient et le reste de la division de 100 par 16.

Sélectionnez
1 mov eax, 100 ; 100 = 64H

2 mov ebx, 0000000FH ; masque = 16 - 1 = 15 ou F

3 and ebx, eax ; ebx = reste = 4

En utilisant le registre CL il est possible de modifier n'importe quel(s) bit(s) d'une donnée. Voici un exemple qui allume un bit de EAX. Le numéro du bit à allumer est stocké dans BH.

Sélectionnez
1 mov cl, bh ; tout d'abord, construire le nombre pour le OU

2 mov ebx, 1

3 shl ebx, cl ; décalage à gauche cl fois

4 or eax, ebx ; allume le bit

Éteindre un bit est un tout petit peut plus dur.

Sélectionnez
1 mov cl, bh ; tout d'abord, construire le nombre pour le ET

2 mov ebx, 1

3 shl ebx, cl ; décalage à gauche cl fois

4 not ebx ; inverse les bits

5 and eax, ebx ; éteint le bit

Le code pour inverser un bit est laissé en exercice au lecteur.

Il n'est pas rare de voir l'instruction déroutante suivante dans un programme 80x86 :

Sélectionnez
xor eax, eax ; eax = 0

Un nombre auquel on applique un XOR avec lui-même donne toujours zéro.

Cette instruction est utilisée, car son code machine est plus petit que l'instruction MOV correspondante.

IV-C. Éviter les branchements conditionnels

Les processeurs modernes utilisent des techniques très sophistiquées pour exécuter le code le plus rapidement possible. Une technique répandue est appelée exécution spéculative. Cette technique utilise les possibilités de traitement en parallèle du processeur pour exécuter plusieurs instructions à la fois. Les branchements conditionnels posent un problème à ce type de fonctionnement.

Le processeur, en général, ne sait pas si le branchement sera effectué ou pas. Selon qu'il est effectué ou non, un ensemble d'instructions différent sera exécuté. Les processeurs essaient de prévoir si le branchement sera effectué. Si la prévision est mauvaise, le processeur a perdu son temps en exécutant le mauvais code.

Une façon d'éviter ce problème est d'éviter d'utiliser les branchements conditionnels lorsque c'est possible. Le code d'exemple de III.A.5 fournit un exemple simple de la façon de le faire. Dans l'exemple précédent, les bits allumés du registre EAX sont décomptés. Il utilise un branchement pour éviter l'instruction INC. La Figure 3.3 montre comment le branchement peut être retiré en utilisant l'instruction ADC pour ajouter directement le drapeau de retenue.

Image non disponible

Les instructions SETxx fournissent un moyen de retirer les branchements dans certains cas. Ces instructions positionnent la valeur d'un registre ou d'un emplacement mémoire d'un octet à zéro ou à un selon l'état du registre FLAGS. Les caractères après SET sont les mêmes que pour les branchements conditionnels. Si la condition correspondante au SETxx est vraie, le résultat stocké est un, s'il est faux, zéro est stocké. Par exemple :

Sélectionnez
setz al ; AL = 1 si ZF est allume, sinon 0

En utilisant ces instructions, il est possible de développer des techniques ingénieuses qui calculent des valeurs sans branchement.

Par exemple, considérons le problème de la recherche de la plus grande de deux valeurs. L'approche standard pour résoudre ce problème serait d'utiliser un CMP et un branchement conditionnel pour déterminer la valeur la plus grande. Le programme exemple ci-dessous montre comment le maximum peut être trouvé sans utiliser aucun branchement.

Sélectionnez
1 ; file : max.asm
2 %include "asm_io.inc"
3 segment .data
4
5  message1 db "Entrez un nombre : ",0
6 message2 db "Entrez un autre nombre : ", 0
7 message3 db "Le plus grand nombre est : ", 0
8
9  segment .bss
10
11 input1 resd 1 ; premier nombre entre
12
13 segment .text
14 global _asm_main
15 _asm_main :
16 enter 0,0 ; routine d'initialisation
17 pusha
18
19 mov eax, message1 ; affichage du premier message
20 call print_string
21 call read_int ; saisie du premier nombre
22 mov [input1], eax
23
24 mov eax, message2 ; affichage du second message
25 call print_string
26 call read_int ; saisie du second nombre (dans eax)
27
28 xor ebx, ebx ; ebx = 0
29 cmp eax, [input1] ; compare le premier et le second nombre
30 setg bl ; ebx = (input2 > input1) ? 1 : 0
31 neg ebx ; ebx = (input2 > input1) ? 0xFFFFFFFF : 0
32 mov ecx, ebx ; ecx = (input2 > input1) ? 0xFFFFFFFF : 0
33 and ecx, eax ; ecx = (input2 > input1) ? input2 : 0
34 not ebx ; ebx = (input2 > input1) ? 0 : 0xFFFFFFFF
35 and ebx, [input1] ; ebx = (input2 > input1) ? 0 : input1
36 or ecx, ebx ; ecx = (input2 > input1) ? input2 : input1
37
38 mov eax, message3 ; affichage du résultat
39 call print_string
40 mov eax, ecx
41 call print_int
42 call print_nl
43
44 popa
45 mov eax, 0 ; retour au C
46 leave
47 ret

L'astuce est de créer un masque de bits qui peut être utilisé pour sélectionner la valeur correcte pour le maximum. L'instruction SETG à la ligne 30 positionne BL à 1 si la seconde saisie est la plus grande ou à 0 sinon. Ce n'est pas le masque de bits désiré.

Pour créer le masque nécessaire, la ligne 31 utilise l'instruction NEG sur le registre EBX (notez que EBX a été positionné à 0 précédemment). Si EBX vaut 0, cela ne fait rien ; cependant, si EBX vaut 1, le résultat est la représentation en complément à 2 de -1 soit 0xFFFFFFFF. C'est exactement le masque de bits désiré.

Le code restant utilise ce masque de bits pour sélectionner la saisie correspondant au plus

grand nombre.

Une autre astuce est d'utiliser l'instruction DEC. Dans le code ci-dessus, si NEG est remplacé par DEC, le résultat sera également 0 ou 0xFFFFFFFF. Cependant, les valeurs sont inversées par rapport à l'utilisation de l'instruction NEG.

IV-D. Manipuler les bits en C

IV-D-1. Les opérateurs niveau bit du C

Contrairement à certains langages de haut niveau, le C fournit des opérateurs pour les opérations niveau bit. L'opération ET est représentée par l'opérateur binaire &(14). L'opération OU est représentée par l'opérateur binaire |. L'opération XOR est représentée par l'opérateur binaire ^ . Et l'opération NOT est représentée par l'opérateur unaire ~ .

Les opérations de décalage sont effectuées au moyen des opérateurs binaires et du C. L'opérateur effectue les décalages à gauche et l'opérateur effectue les décalages à droite. Ces opérateurs prennent deux opérandes. L'opérande de gauche est la valeur à décaler et l'opérande de droite est le nombre de bits à décaler. Si la valeur à décaler est d'un type non signé, un décalage logique est effectué. Si la valeur est d'un type signé (comme int), alors un décalage arithmétique est utilisé. Voici un exemple en C utilisant ces opérateurs :

Sélectionnez
1 short int s ; / on suppose que les short int font 16 bits /

2 short unsigned u;

3 s = &#8722;1; / s = 0xFFFF (complement a 2) /

4 u = 100; / u = 0x0064 /
5 u = u | 0x0100; / u = 0x0164 /
6 s = s & 0xFFF0; / s = 0xFFF0 /
7 s = s ^ u; / s = 0xFE94 /
8 u = u << 3; / u = 0x0B20 (décalage logique) /
9 s = s >> 2; / s = 0xFFA5 (décalage arithmetique) /

IV-D-2. Utiliser les opérateurs niveau bit en C

Les opérateurs niveau bit sont utilisés en C pour les mêmes raisons qu'ils le sont en assembleur. Ils permettent de manipuler les bits d'une donnée individuellement et peuvent être utilisés pour des multiplications et des divisions rapides. En fait, un compilateur C malin utilisera automatiquement un décalage pour une multiplication du type x *= 2.

Beaucoup d'API(15) de systèmes d'exploitation (comme POSIX(16) et Win32) contiennent des fonctions qui utilisent des opérandes donc les données sont codées sous la forme de bits. Par exemple, les systèmes POSIX conservent les permissions sur les fichiers pour trois différents types d'utilisateurs : utilisateur (un nom plus approprié serait propriétaire), groupe et autres. Chaque type d'utilisateur peut recevoir la permission de lire, écrire et/ou exécuter un fichier. Pour changer les permissions d'un fichier, le programmeur C doit manipuler des bits individuels. POSIX définit plusieurs macros pour l'aider (voir Tableau 3.6).

Image non disponible

La fonction chmod peut être utilisée pour définir les permissions sur un fichier. Cette fonction prend deux paramètres, une chaine avec le nom du fichier à modifier et un entier(17) avec les bits appropriés d'allumés pour les permissions désirées. Par exemple, le code ci-dessous définit les permissions pour permettre au propriétaire du fichier de lire et d'écrire dedans, au groupe de lire le fichier et d'interdire l'accès aux autres.

Sélectionnez
chmod("foo", S_IRUSR | S_IWUSR | S_IRGRP );

La fonction POSIX stat peut être utilisée pour récupérer les bits de permission en cours pour un fichier. En l'utilisant avec la fonction chmod,

il est possible de modifier certaines des permissions sans changer les autres.

Voici un exemple qui retire l'accès en écriture aux autres et ajoute les droits de lecture pour le propriétaire. Les autres permissions ne sont pas altérées.

Sélectionnez
1 struct stat le_stats ; / structure utilisee par stat () /

2 stat ("foo", & le_stats ); / lit les infos du fichier

3 le_stats .st_mode contient les bits de permission /

4 chmod("foo", ( le_stats .st_mode & ~S_IWOTH) | S_IRUSR);

IV-E. Représentations big et little endian

Le Chapitre II a introduit les concepts de représentations big et little endian des données multioctets. Cependant, l'auteur s'est rendu compte que pour beaucoup de gens, ce sujet est confus. Cette section couvre le sujet plus en détail.

Le lecteur se souvient sûrement que le caractère big ou little endian fait référence à l'ordre dans lequel les octets (pas les bits) d'un élément de données multioctets sont stockés en mémoire. La représentation big endian est la méthode la plus intuitive. Elle stocke l'octet le plus significatif en premier, puis le second octet le plus significatif, etc. En d'autres termes, les gros (big) bits sont stockés en premier. La méthode little endian stocke les octets dans l'ordre inverse (moins significatif en premier). La famille des processeurs x86 utilise la représentation little endian.

Par exemple, considérons le double mot représentant 1234567816. En représentation big endian, les octets seraient stockés 12 34 56 78. En représentation little endian, les octets seraient stockés 78 56 34 12.

Le lecteur est probablement en train de se demander pourquoi n'importe quel concepteur de puce sain d'esprit utiliserait la représentation little endian ?

Les ingénieurs de chez Intel étaient-ils sadiques pour infliger à une multitude de programmeurs cette représentation qui prête à confusion ? Il peut sembler que le processeur ait à faire du travail supplémentaire pour stocker les octets en mémoire dans l'ordre inverse (et pour les réinverser lorsqu'il lit à partir de la mémoire). En fait, le processeur ne fait aucun travail supplémentaire pour lire et écrire en mémoire en utilisant le format little endian. Il faut comprendre que le processeur est constitué de beaucoup de circuits électroniques qui ne travaillent que sur des valeurs de un bit. Les bits (et les octets) peuvent être dans n'importe quel ordre dans le processeur.

Considérons le registre de deux octets AX. Il peut être décomposé en deux registres d'un octet : AH et AL. Il y a des circuits dans le processeur qui conservent les valeurs de AH and AL. Ces circuits n'ont pas d'ordre dans le processeur. C'est-à-dire que les circuits pour AH ne sont pas avant ou après les circuits pour AL. Une instruction MOV qui copie la valeur de AX en mémoire copie la valeur de AL puis de AH. Ce n'est pas plus dur pour le processeur que de stocker AH en premier.

Le même argument s'applique aux bits individuels d'un octet. Ils ne sont pas réellement dans un ordre déterminé dans les circuits du processeur (ou en mémoire en ce qui nous concerne). Cependant, comme les bits individuels ne peuvent pas être adressés dans le processeur ou en mémoire, il n'y a pas de façon de savoir (et aucune raison de s'en soucier) l'ordre dans lequel ils sont conservés à l'intérieur du processeur.

Le code C de la Figure 3.4 montre comment le caractère big ou little endian d'un processeur peut être déterminé.

Image non disponible

Le pointeur p traite la variable word comme un tableau de caractères de deux éléments. Donc, p[0] correspond au premier octet de word en mémoire dont la valeur dépend du caractère big ou little endian du processeur.

IV-E-1. Quand se soucier du caractère big ou little endian

Pour la programmation courante, le caractère big ou little endian du processeur n'est pas important. Le moment le plus courant où cela devient important est lorsque des données binaires sont transférées entre différents systèmes informatiques. Cela se fait habituellement en utilisant un type quelconque de média physique (comme un disque) ou via un réseau. Comme les données ASCII sont sur un seul octet, le caractère big ou little endian n'est pas un problème.

Avec l'avènement des jeux de caractères multioctets comme UNICODE, le caractère big ou little endian devient important même pour les données texte. UNICODE supporte les deux types de représentation et a un mécanisme pour indiquer celle qui est utilisée pour représenter les données.

Tous les en-têtes internes de TCP/IP stockent les entiers au format big

endian (appelé ordre des octets réseau). Les bibliothèques TCP/IP offrent des fonctions C pour résoudre les problèmes de représentation big ou little endian d'une façon portable. Par exemple, la fonction htonl () convertit un double-mot (ou un entier long) depuis le format hôte vers le format réseau.

La fonction function ntohl () effectue la transformation inverse(18). Pour un système big endian les deux fonctions retournent leur paramètre inchangé. Cela permet d'écrire des programmes réseau qui compileront et s'exécuteront correctement sur n'importe quel système sans tenir compte de la représentation utilisée. Pour plus d'information sur les représentations big et little endian et la programmation réseau, voyez l'excellent livre de W. Richard Steven : UNIX Network Programming.

La Figure 3.5 montre une fonction C qui passe d'une représentation à l'autre pour un double mot.

Image non disponible

Le processeur 486 a introduit une nouvelle instruction machine appelée BSWAP qui inverse les octets de n'importe quel registre 32 bits. Par exemple,

Sélectionnez
bswap edx ; échange les octets de edx

L'instruction ne peut pas être utilisée sur des registres de 16 bits. Cependant, l'instruction XCHG peut être utilisée pour échanger les octets des registres 16 bits pouvant être décomposés en registres de 8 bits. Par exemple :

Sélectionnez
xchg ah,al ; échange les octets de ax

IV-F. Compter les bits

Plus haut, nous avons donné une technique intuitive pour compter le nombre de bits allumés dans un double-mot. Cette section décrit d'autres méthodes moins directes de le faire pour illustrer les opérations sur les bits dont nous avons parlé dans ce chapitre.

IV-F-1. Méthode une

La première méthode est très simple, mais pas évidente. La Figure 3.6 en montre le code.

Image non disponible

Comment fonctionne cette méthode ? À chaque itération de la boucle, un bit est éteint dans data. Quand tous les bits sont éteints ((i.e. lorsque data vaut zéro), la boucle s'arrête. Le nombre d'itérations requises pour mettre data à zéro est égal au nombre de bits dans la valeur originale de data.

La ligne 6 est l'endroit où un bit de data est éteint. Comment cela marche ? Considérons la forme générale de la représentation binaire de data et le 1 le plus à droite dans cette représentation. Par dénifition, chaque bit après ce 1 est à zéro. Maintenant, que sera la représentation de data - 1 ? Les bits à gauche du 1 le plus à droite seront les mêmes que pour data, mais à partir du 1 le plus à droite, les bits seront les compléments des bits originaux de data. Par exemple :

data = xxxxx10000

data - 1 = xxxxx01111

où les x sont les mêmes pour les deux nombres. Lorsque l'on applique un ET sur data avec data - 1, le résultat mettra le 1 le plus à droite de data à zéro et laissera les autres bits inchangés.

IV-F-2. Méthode deux

Un tableau de recherche peut également être utilisé pour contrer les bits de n'importe quel double-mot. L'approche intuitive serait de précalculer le nombre de bits de chaque double-mot et de le stocker dans un tableau.

Cependant, il y a deux problèmes relatifs à cette approche. Il y a à peu près 4 milliards de valeurs de doubles-mots ! Cela signifie que le tableau serait très gros et que l'initialiser prendrait beaucoup de temps (en fait, à moins que l'on ait l'intention d'utiliser le tableau plus de 4 milliards de fois, cela prendrait plus de temps de l'initialiser que cela n'en prendrait de calculer le nombre de bits avec la méthode un !).

Une méthode plus réaliste calculerait le nombre de bits pour toutes les valeurs possibles d'octet et les stockerait dans un tableau. Le double-mot peut alors être décomposé en quatre valeurs d'un octet. Le nombre de bits pour chacune de ces quatre valeurs d'un octet est recherché dans le tableau et additionné aux autres pour trouver le nombre de bits du double-mot original.

La Figure 3.7 montre le code implémentant cette approche.

Image non disponible

La fonction initialize_count_bits doit être appelée avant le premier appel à la fonction count_bits. Cette fonction initialise le tableau global byte_bit_count. La fonction count_bits ne considère pas la variable data comme un double-mot, mais comme un tableau de quatre octets. Donc, dword[0] est un des octets de data (soit le moins significatif, soit le plus significatif selon que le matériel est little ou big endian, respectivement).

Bien sûr, on peut utiliser une instruction comme :

(data >> 24) & 0x000000FF

pour trouver la valeur de l'octet le plus significatif et des opérations similaires pour les autres octets, cependant, ces opérations seraient plus lentes qu'une référence à un tableau.

Un dernier point, une boucle for pourrait facilement être utilisée pour calculer la somme des lignes 22 et 23. Mais, cela inclurait le supplément de l'initialisation de l'indice, sa comparaison après chaque itération et son incrémentation. Calculer la somme comme une somme explicite de quatre valeurs est plus rapide. En fait, un compilateur intelligent convertirait la version avec une boucle for en une somme explicite. Ce procédé de réduire ou éliminer les itérations d'une boucle est une technique d'optimisation de compilateur appelée loop unrolling (déroulage de boucle).

IV-F-3. Méthode trois

Il y a encore une méthode intelligente de compter les bits allumés d'une donnée. Cette méthode ajoute littéralement les 1 et les 0 de la donnée.

La somme est égale au nombre de 1 dans la donnée. Par exemple, considérons le comptage des 1 d'un octet stocké dans une variable nommée data. La première étape est d'effectuer l'opération suivante :

Sélectionnez
data = (data & 0x55) + ((data >> 1) & 0x55);

Pourquoi faire cela ? La constante hexa 0x55 vaut 01010101 en binaire. Dans le premier opérande de l'addition, on effectue un ET sur data avec elle, les bits sur des positions impaires sont supprimés. Le second opérande ((data>> 1) & 0x55) commence par déplacer tous les bits à des positions paires vers une position impaire et utilise le même masque pour supprimer ces bits.

Maintenant, le premier opérande contient les bits impairs et le second, les bits pairs de data. Lorsque ces deux opérandes sont additionnés, les bits pairs et les bits impairs de data sont additionnés. Par exemple, si data vaut 101100112, alors :

Image non disponible

L'addition à droite montre les bits additionnés ensemble. Les bits de l'octet sont divisés en deux champs de 2 bits pour montrer qu'il y a en fait quatre additions indépendantes. Comme la plus grande valeur que ces sommes peuvent prendre est deux, il n'est pas possible qu'une somme déborde de son champ et corrompe l'une des autres sommes.

Bien sûr, le nombre total de bits n'a pas encore été calculé. Cependant, la même technique que celle qui a été utilisée ci-dessus peut être utilisée pour calculer le total en une série d'étapes similaires. L'étape suivante serait :

data = (data & 0x33) + ((data >> 2) & 0x33);

En continuant l'exemple du dessus (souvenez-vous que data vaut maintenant 011000102).

Image non disponible

Il y a maintenant deux champs de 4 bits additionnés individuellement. La prochaine étape est d'additionner ces deux sommes de bits ensemble pour former le résultat final :

data = (data & 0x0F) + ((data >> 4) & 0x0F);

En utilisant l'exemple ci-dessus (avec data égale à 001100102) :

Image non disponible

Maintenant, data vaut 5 ce qui est le résultat correct. La Figure 3.8 montre une implémentation de cette méthode qui compte les bits dans un double-mot.

Image non disponible

Elle utilise une boucle for pour calculer la somme. Il serait plus rapide de dérouler la boucle, cependant, la boucle rend plus claire la façon dont la méthode se généralise à différentes tailles de données.

V. Sous-programmes

Ce chapitre explique comment utiliser des sous-programmes pour créer des programmes modulaires et s'interfacer avec des langages de haut niveau (comme le C). Les fonctions et les procédures sont des exemples de sous-programmes dans les langages de haut niveau.

Le code qui appelle le sous-programme et le sous-programme lui-même doivent se mettre d'accord sur la façon de se passer les données. Ces règles sur la façon de passer les données sont appelées conventions d'appel. Une grande partie de ce chapitre traitera des conventions d'appel standard du C qui peuvent être utilisées pour interfacer des sous-programmes assembleur avec des programmes C. Celles-ci (et d'autres conventions) passent souvent les adresses des données (i.e. des pointeurs) pour permettre au sous-programme d'accéder aux données en mémoire.

V-A. Adressage indirect

L'adressage indirect permet aux registres de se comporter comme des pointeurs. Pour indiquer qu'un registre est utilisé indirectement comme un pointeur, il est entouré par des crochets ([ ]). Par exemple :

Sélectionnez
1 mov ax, [Data] ; adressage mémoire direct normal d'un mot

2 mov ebx, Data ; ebx = & Data

3 mov ax, [ebx] ; ax = *ebx

Comme AX contient un mot, la ligne 3 lit un mot commençant à l'adresse stockée dans EBX. Si AX était remplacé par AL, un seul octet serait lu.

Il est important de réaliser que les registres n'ont pas de types comme les variables en C. Ce sur quoi EBX est censé pointer est totalement déterminé par les instructions utilisées. Si EBX est utilisé de manière incorrecte, il n'y aura souvent pas d'erreur signalée de la part de l'assembleur, cependant, le programme ne fonctionnera pas correctement. C'est une des nombreuses raisons pour lesquelles la programmation assembleur est plus sujette à erreur que la programmation de haut niveau.

Tous les registres 32 bits généraux (EAX, EBX, ECX, EDX) et d'index (ESI, EDI) peuvent être utilisés pour l'adressage indirect. En général, les registres 16 et 8 bits ne le peuvent pas.

V-B. Exemple de sous-programme simple

Un sous-programme est une unité de code indépendante qui peut être utilisée depuis différentes parties du programme. En d'autres termes, un sous-programme est comme une fonction en C. Un saut peut être utilisé pour appeler le sous-programme, mais le retour présente un problème. Si le sous-programme est destiné à être utilisé par différentes parties du programme, il doit revenir à la section de code qui l'a appelé. Donc, le retour du sous-programme ne peut pas être codé en dur par un saut vers une étiquette. Le code ci-dessous montre comment cela peut être réalisé en utilisant une forme indirecte de l'instruction JMP. Cette forme de l'instruction utilise la valeur d'un registre pour déterminer où sauter (donc, le registre agit plus comme un pointeur de fonction du C). Voici le premier programme du Chapitre II réécrit pour utiliser un sous-programme.

sub1.asm
Sélectionnez
1 ; fichier : sub1.asm
2 ; Programme d'exemple de sous-programme
3 %include "asm_io.inc"
4
5  segment .data
6 prompt1 db "Entrez un nombre : ", 0 ; ne pas oublier le zéro terminal
7 prompt2 db "Entrez un autre nombre : ", 0
8 outmsg1 db "Vous avez entré ", 0
9 outmsg2 db " et ", 0
10 outmsg3 db ", la somme des deux vaut ", 0
11
12 segment .bss
13 input1 resd 1
14 input2 resd 1
15
16 segment .text
17 global _asm_main
18 _asm_main :
19 enter 0,0 ; routine d'initialisation
20 pusha
21
22 mov eax, prompt1 ; affiche l'invite
23 call print_string
24
25 mov ebx, input1 ; stocke l'adresse de input1 dans ebx
26 mov ecx, ret1 ; stocke l'adresse de retour dans ecx
27 jmp short get_int ; lit un entier
28 ret1:
29 mov eax, prompt2 ; affiche l'invite
30 call print_string
31
32 mov ebx, input2
33 mov ecx, $ + 7 ; ecx = cette addresse + 7
34 jmp short get_int
35
36 mov eax, [input1] ; eax = dword dans input1
37 add eax, [input2] ; eax += dword dans input2
38 mov ebx, eax ; ebx = eax
39
40 mov eax, outmsg1
41 call print_string ; affiche le premier message
42 mov eax, [input1]
43 call print_int ; affiche input1
44 mov eax, outmsg2
45 call print_string ; affiche le second message
46 mov eax, [input2]
47 call print_int ; affiche input2
48 mov eax, outmsg3
49 call print_string ; affiche le troisième message
50 mov eax, ebx
51 call print_int ; affiche la somme (ebx)
52 call print_nl ; retour à la ligne
53
54 popa
55 mov eax, 0 ; retour au C
56 leave
57 ret
58 ; subprogram get_int
59 ; Paramètres :
60 ; ebx - addresse du dword dans lequel stocker l'entier
61 ; ecx - addresse de l'instruction vers laquelle retourner
62 ; Notes :
63 ; la valeur de eax est perdue
64 get_int:
65 call read_int
66 mov [ebx], eax ; stocke la saisie en mémoire
67 jmp ecx sub1.;asrmetour à l'appelant

Le sous-programme get_int utilise une convention d'appel simple, basée sur les registres. Il s'attend à ce que le registre EBX contienne l'adresse du DWORD dans lequel stocker le nombre saisi et à ce que le registre ECX contiennent l'adresse de l'instruction vers laquelle retourner.

Dans les lignes 25 à 28, l'étiquette ret1 est utilisée pour calculer cette adresse de retour.

Dans les lignes 32 à 34, l'opérateur $ est utilisé pour calculer l'adresse de retour. L'opérateur $ retourne l'adresse de la ligne sur laquelle il apparaît. L'expression $ + 7 calcule l'adresse de l'instruction MOV de la ligne 36.

Ces deux calculs d'adresses de code de retour sont compliqués. La première méthode requiert la définition d'une étiquette pour tout appel de sous-programme.

La seconde méthode ne requiert pas d'étiquette, mais nécessite de réfléchir attentivement. Si un saut proche avait été utilisé à la place d'un saut court, le nombre à ajouter à $ n'aurait pas été 7 !

Heureusement, il y a une façon plus simple d'appeler des sous-programmes. Cette méthode utilise la pile.

V-C. La pile

Beaucoup de processeurs ont un support intégré pour une pile. Une pile est une liste Last-In First-Out (LIFO, dernier entré, premier sorti). La pile est une zone de la mémoire qui est organisée de cette façon. L'instruction PUSH ajoute des données à la pile et l'instruction POP retire une donnée. La donnée retirée est toujours la dernière donnée ajoutée (c'est pourquoi on appelle ce genre de liste dernier entré, premier sorti).

Le registre de segment SS spécifie le segment qui contient la pile (habituellement c'est le même que celui qui contient les données). Le registre ESP contient l'adresse de la donnée qui sera retirée de la pile. On dit que cette donnée est au sommet de la pile. Les données ne peuvent être ajoutées que par unités de doubles-mots. C'est-à-dire qu'il est impossible de placer un octet seul sur la pile.

L'instruction PUSH insère un double-mot(19) sur la pile en ôtant 4 de ESP puis en stockant le double-mot en [ESP]. L'instruction POP lit le double-mot en [ESP] puis ajoute 4 à ESP. Le code ci-dessous montre comment fonctionnent ces instructions en supposant que ESP vaut initialement 1000H.

Sélectionnez
1 push dword 1 ; 1 est stocké en 0FFCh, ESP = 0FFCh

2 push dword 2 ; 2 est stocké en 0FF8h, ESP = 0FF8h

3 push dword 3 ; 3 est stocké en 0FF4h, ESP = 0FF4h

4 pop eax ; EAX = 3, ESP = 0FF8h

5 pop ebx ; EBX = 2, ESP = 0FFCh

6 pop ecx ; ECX = 1, ESP = 1000h

La pile peut être utilisée comme un endroit approprié pour stocker des données temporairement. Elle est également utilisée pour effectuer des appels de sous-programmes, passer des paramètres et des variables locales.

Le 80x86 fournit également une instruction, PUSHA, qui empile les valeurs des registres EAX, EBX, ECX, EDX, ESI, EDI et EBP (pas dans cet ordre).

L'instruction POPA peut être utilisée pour les dépiler tous.

V-D. Les Instructions CALL et RET

Le 80x86 fournit deux instructions qui utilisent la pile pour effectuer des appels de sous-programmes rapidement et facilement. L'instruction CALL effectue un saut inconditionnel vers un sous-programme et empile l'adresse de l'instruction suivante. L'instruction RET dépile une adresse et saute à cette adresse. Lors de l'utilisation de ces instructions, il est très important de gérer la pile correctement afin que le chiffre correct soit dépilé par l'instruction RET !

Le programme précédent peut être réécrit pour utiliser ces nouvelles instructions en changeant les lignes 25 à 34 en ce qui suit :

Sélectionnez
mov ebx, input1

call get_int

mov ebx, input2

call get_int

et en changeant le sous-programme get_int en :

Sélectionnez
get_int :

call read_int

mov [ebx], eax

ret

Il y a plusieurs avantages à utiliser CALL et RET :

  • c'est plus simple ! ;
  • cela permet d'imbriquer des appels de sous-programmes facilement.

Notez que get_int appelle read_int. Cet appel empile une autre adresse à la fin du code de read_int se trouve un RET qui dépile l'adresse de retour et saute vers le code de get_int. Puis, lorsque le RET de get_int est exécuté, il dépile l'adresse de retour qui revient vers asm_main. Cela fonctionne correctement, car il s'agit d'une pile LIFO.

Souvenez-vous, il est très important de dépiler toute donnée qui est empilée. Par exemple, considérons le code suivant :

Sélectionnez
1 get_int :

2 call read_int

3 mov [ebx], eax

4 push eax

5 ret ; dépile la valeur de EAX, pas l'adresse de retour !!

Ce code ne reviendra pas correctement !

V-E. Conventions d'appel

Lorsqu'un sous-programme est appelé, le code appelant et le sous-programme (l'appelé) doivent s'accorder sur la façon de se passer les données. Les langages de haut niveau ont des manières standard de passer les données appelées « conventions d'appel ». Pour interfacer du code de haut niveau avec le langage assembleur, le code assembleur doit utiliser les mêmes conventions que le langage de haut niveau. Les conventions d'appel peuvent différer d'un compilateur à l'autre ou peuvent varier selon la façon dont le code est compilé (p.e. selon que les optimisations sont activées ou pas). Une convention universelle est que le code est appelé par une instruction CALL et revient par un RET.

Tous les compilateurs C PC supportent une convention d'appel qui sera décrite dans le reste de ce chapitre par étape. Ces conventions permettent de créer des sous-programmes réentrants. Un sous-programme réentrant peut être appelé depuis n'importe quel endroit du programme en toute sécurité (même depuis le sous-programme lui-même).

V-E-1. Passer les paramètres via la pile

Les paramètres d'un sous-programme peuvent être passés par la pile. Ils sont empilés avant l'instruction CALL. Comme en C, si le paramètre doit être modifié par le sous-programme, l'adresse de la donnée doit être passée, pas sa valeur. Si la taille du paramètre est inférieure à un double-mot, il doit être converti en un double-mot avant d'être empilé.

Les paramètres sur la pile ne sont pas dépilés par le sous-programme, à place, ils sont accédés depuis la pile elle-même. Pourquoi ? Comme ils doivent être empilés avant l'instruction CALL, l'adresse de retour devrait être dépilée avant tout (puis réempilée).

Souvent, les paramètres sont utilisés à plusieurs endroits dans le sous-programme. Habituellement, ils ne peuvent pas être conservés dans un registre durant toute la durée du sous-programme et devront être stockés en mémoire. Les laisser sur la pile conserve une copie de la donnée en mémoire qui peut être accédée depuis n'importe quel endroit du sous-programme.

Considérons un sous-programme auquel on passe un paramètre unique via la pile. Lorsque le sous-programme est appelé, la pile ressemble à la Figure 4.1. On peut accéder au paramètre en utilisant l'adressage indirect ([ESP+4](20)).

Image non disponible

Lors de l'utilisation de l'adressage indirect, le processeur 80x86 accède à différents segments selon les registres utilisés dans l'expression d'adressage indirect. ESP (et EBP) utilisent le segment de pile alors que EAX, EBX, ECX et EDX utilisent le segment de données.
Cependant, ce n'est habituellement pas important pour la plupart des programmes en mode protégé, car pour eux, les segments de pile et de données sont les mêmes.

Si la pile est également utilisée dans le sous-programme pour stocker des données, le nombre à ajouter à ESP changera. Par exemple, la Figure 4.2 montre à quoi ressemble la pile si un DWORD est empilé.

Image non disponible

Maintenant, le paramètre se trouve en ESP + 8, plus en ESP + 4. Donc, utiliser ESP lorsque l'on fait référence à des paramètres peut être une source d'erreurs. Pour résoudre ce problème, le 80386 fournit un autre registre à utiliser : EBP. La seule utilité de ce registre est de faire référence à des données sur la pile. La convention d'appel C stipule qu'un sous-programme doit d'abord empiler la valeur de EBP puis définir EBP pour qu'il soit égal à ESP. Cela permet à ESP de changer au fur et à mesure que des données sont empilées ou dépilées sans modifier EBP. À la fin du programme, la valeur originale de EBP doit être restaurée (c'est pourquoi elle est sauvegardée au début du sous-programme).

La Figure 4.3 montre la forme générale d'un sous-programme qui suit ces conventions.

Image non disponible

Les lignes 2 et 3 de la Figure 4.3 constituent le prologue générique d'un sous-programme. Les lignes 5 et 6 constituent l'épilogue. La Figure 4.4 montre à quoi ressemble la pile immédiatement après le prologue.

Image non disponible

Maintenant, le paramètre peut être accédé avec [EBP + 8] depuis n'importe quel endroit du programme sans se soucier de ce qui a été empilé entretemps par le sous-programme.

Une fois le sous-programme terminé, les paramètres qui ont été empilés doivent être retirés. La convention d'appel C spécifie que c'est au code appelant de le faire. D'autres conventions sont différentes. Par exemple, la convention d'appel Pascal spécifie que c'est au sous-programme de retirer les paramètres (Il y a une autre forme de l'instruction RET qui permet de le faire facilement). Quelques compilateurs C supportent cette convention également.

Le mot-clé pascal est utilisé dans le prototype et la définition de la fonction pour indiquer au compilateur d'utiliser cette convention. En fait, la convention stdcall, que les fonctions de l'API C MS Windows utilisent, fonctionne également de cette façon. Quel est son avantage ? Elle est un petit peu plus efficace que la convention C. Pourquoi toutes les fonctions C n'utilisent-elles pas cette convention alors ? En général, le C autorise une fonction à avoir un nombre variable d'arguments (p.e., les fonctions printf et scanf).

Pour ce type de fonction, l'opération consistant à retirer les paramètres de la pile varie d'un appel à l'autre. La convention C permet aux instructions nécessaires à la réalisation de cette opération de varier facilement d'un appel à l'autre. Les conventions Pascal et stdcall rendent cette opération très compliquée. Donc, la convention Pascal (comme le langage Pascal) n'autorise pas ce type de fonction. MS Windows peut utiliser cette convention puisqu'aucune de ses fonctions d'API ne prend un nombre variable d'arguments.

La Figure 4.5 montre comment un sous-programme utilisant la convention d'appel C serait appelé.

Image non disponible

La ligne 3 retire le paramètre de la pile en manipulant directement le pointeur de pile. Une instruction POP pourrait également être utilisée, mais cela nécessiterait le stockage d'un paramètre inutile dans un registre. En fait, dans ce cas particulier, beaucoup de compilateurs utilisent une instruction POP ECX pour retirer le paramètre. Le compilateur utilise un POP plutôt qu'un ADD, car le ADD nécessite plus d'octets pour stocker l'instruction. Cependant, le POP change également la valeur de ECX ! Voici un autre programme exemple avec deux sous-programmes qui utilisent les conventions d'appel C dont nous venons de parler. La ligne 54 (et les autres) montre que plusieurs segments de données et de texte peuvent être déclarés dans un même fichier source. Ils seront combinés en des segments de données et de texte uniques lors de l'édition de liens. Diviser les données et le code en segments séparés permet d'avoir les données d'un sous-programme définies à proximité de celui-ci.

sub3.asm
Sélectionnez
1 %include "asm_io.inc"
2
3  segment .data
4 sum dd 0
5
6  segment .bss
7 input resd 1
8
9 ;
10 ; algorithme en pseudo-code
11 ; i = 1;
12 ; sum = 0;
13 ; while( get_int(i, &input), input != 0 ) {
14 ; sum += input;
15 ; i++;
16 ; }
17 ; print_sum(num);
18 segment .text
19 global _asm_main
20 _asm_main :
21 enter 0,0 ; routine d'initialisation
22 pusha
23
24 mov edx, 1 ; edx est le 'i' du pseudo-code
25 while_loop :
26 push edx ; empile i
27 push dword input ; empile l'adresse de input
28 call get_int
29 add esp, 8 ; dépile i et &input
30
31 mov eax, [input]
32 cmp eax, 0
33 je end_while
34
35 add [sum], eax ; sum += input
36
37 inc edx
38 jmp short while_loop
39
40 end_while:
41 push dword [sum] ; empile la valeur de sum
42 call print_sum
43 pop ecx ; dépile [sum]
44
45 popa
46 leave
47 ret
48
49 ; sous-programme get_int
50 ; Paramètres (dans l'ordre de l'empilement)
51 ; nombre de saisies (en [ebp + 12])
52 ; adresse du mot où stocker la saisie (en [ebp + 8])
53 ; Notes :
54 ; les valeurs de eax et ebx sont détruites
55 segment .data
56 prompt db ") Entrez un nombre entier (0 pour quitter): ", 0
57
58 segment .text
59 get_int:
60 push ebp
61 mov ebp, esp
62
63 mov eax, [ebp + 12]
64 call print_int
65
66 mov eax, prompt
67 call print_string
68
69 call read_int
70 mov ebx, [ebp + 8]
71 mov [ebx], eax ; stocke la saisie en mémoire
72
73 pop ebp
74 ret ; retour à l'appelant
75
76 ; sous-programme print_sum
77 ; affiche la somme
78 ; Paramètre :
79 ; somme à afficher (en [ebp+8])
80 ; Note : détruit la valeur de eax
81 ;
82 segment .data
83 result db "La somme vaut ", 0
84
85 segment .text
86 print_sum:
87 push ebp
88 mov ebp, esp
89
90 mov eax, result
91 call print_string
92
93 mov eax, [ebp+8]
94 call print_int
95 call print_nl
96
97 pop ebp
98 ret

V-E-2. Variables locales sur la pile

La pile peut être utilisée comme un endroit pratique pour stocker des variables locales. C'est exactement ce que fait le C pour les variables normales (ou automatiques en C lingo). Utiliser la pile pour les variables est important si l'on veut que les sous-programmes soient réentrants. Un programme réentrant fonctionnera qu'il soit appelé de n'importe quel endroit, même à partir du sous-programme lui-même. En d'autres termes, les sous-programmes réentrants peuvent être appelés récursivement.

Utiliser la pile pour les variables économise également de la mémoire. Les données qui ne sont pas stockées sur la pile utilisent de la mémoire du début à la fin du programme (le C appelle ce type de variables global ou static). Les données stockées sur la pile n'utilisent de la mémoire que lorsque le sous-programme dans lequel elles sont définies est actif.

Les variables locales sont stockées immédiatement après la valeur de EBP sauvegardée dans la pile. Elles sont allouées en soustrayant le nombre d'octets requis de ESP dans le prologue du sous-programme. La Figure 4.6 montre le nouveau squelette du sous-programme.

Image non disponible

Le registre EBP est utilisé pour accéder à des variables locales. Considérons la fonction C de la Figure 4.7.

Image non disponible

La Figure 4.8 montre comment le sous-programme équivalent pourrait être écrit en assembleur.

Image non disponible

La Figure 4.9 montre à quoi ressemble la pile après le prologue du programme de la Figure 4.8.

Image non disponible

Cette section de la pile qui contient les paramètres, les informations de retour et les variables locales est appelée cadre de pile (stack frame). Chaque appel de fonction C crée un nouveau cadre de pile sur la pile.

Le prologue et l'épilogue d'un sous-programme peuvent être simplifiés en utilisant deux instructions spéciales qui sont conçues spécialement dans ce but. L'instruction ENTER effectue le prologue et l'instruction LEAVE l'épilogue.

L'instruction ENTER prend deux opérandes immédiats. Dans la convention d'appel C, le deuxième opérande est toujours 0. Le premier opérande est le nombre d'octets nécessaires pour les variables locales. L'instruction LEAVE n'a pas d'opérande. La Figure 4.10 montre comment ces instructions sont utilisées. Notez que le squelette de programme (Figure 1.7) utilise également ENTER et LEAVE.

Image non disponible

En dépit du fait que ENTER et LEAVE simplifient le prologue et l'épilogue, ils ne sont pas utilisés très souvent. Pourquoi ? Parce qu'ils sont plus lents que les instructions plus simples équivalentes ! C'est un des exemples où il ne faut pas supposer qu'une instruction est plus rapide qu'une séquence de plusieurs instructions.

V-F. Programme multimodule

Un programme multimodule est un programme composé de plus d'un fichier objet. Tous les programmes présentés jusqu'ici sont des programmes multimodules. Ils consistent en un fichier objet C pilote et le fichier objet assembleur (plus les fichiers objet de la bibliothèque C). Souvenez-vous que l'éditeur de liens combine les fichiers objet en un programme exécutable unique. L'éditeur de liens doit rapprocher toutes les références faites à chaque étiquette d'un module (i.e. un fichier objet) de sa définition dans un autre module. Afin que le module A puisse utiliser une étiquette définie dans le module B, la directive extern doit être utilisée. Après la directive extern vient une liste d'étiquettes délimitées par des virgules. La directive indique à l'assembleur de traiter ces étiquettes comme externes au module. C'est-à-dire qu'il s'agit d'étiquettes qui peuvent être utilisées dans ce module, mais sont définies dans un autre. Le fichier asm_io.inc définit les routines read_int, etc. comme externes.

En assembleur, les étiquettes ne peuvent pas être accédées de l'extérieur par défaut. Si une étiquette doit pouvoir être accédée depuis d'autres modules que celui dans lequel elle est dénie, elle doit être déclarée comme globale dans son module, par le biais de la directive global. La ligne 13 du listing du programme squelette de la Figure 1.7 montre que l'étiquette _asm_main est définie comme globale. Sans cette déclaration, l'éditeur de liens indiquerait une erreur. Pourquoi ? Parce que le code C ne pourrait pas faire référence à l'étiquette interne _asm_main.

Voici le code de l'exemple précédent réécrit afin d'utiliser deux modules. Les deux sous-programmes (get_int et print_sum) sont dans des fichiers source distincts de celui de la routine _asm_main.

main4.asm
Sélectionnez
1 %include "asm_io.inc"
2
3 segment .data
4 sum dd 0
5
6 segment .bss
7 input resd 1
8
9 segment .text
10 global _asm_main
11 extern get_int, print_sum
12 _asm_main :
13 enter 0,0 ; routine d'initialisation
14 pusha
15
16 mov edx, 1 ; edx est le 'i' du pseudo-code
17 while_loop :
18 push edx ; empile i
19 push dword input ; empile l'adresse de input
20 call get_int
21 add esp, 8 ; dépile i et &input
22
23 mov eax, [input]
24 cmp eax, 0
25 je end_while
26
27 add [sum], eax ; sum += input
28
29 inc edx
30 jmp short while_loop
31
32 end_while:
33 push dword [sum] ; empile la valeur de sum
34 call print_sum
35 pop ecx ; dépile [sum]
36
37 popa
38 leave
39 ret
sub4.asm
Sélectionnez
1 %include "asm_io.inc"
2
3  segment .data
4 prompt db ") Entrez un nombre entier (0 pour quitter): ", 0
5
6 segment .text
7 global get_int, print_sum
8 get_int :
9 enter 0,0
10
11 mov eax, [ebp + 12]
12 call print_int
13
14 mov eax, prompt
15 call print_string
16
17 call read_int
18 mov ebx, [ebp + 8]
19 mov [ebx], eax ; stocke la saisie en mémoire
20
21 leave
22 ret ; retour à l'appelant
23
24 segment .data
25 result db "La somme vaut ", 0
26
27 segment .text
28 print_sum :
29 enter 0,0
30
31 mov eax, result
32 call print_string
33
34 mov eax, [ebp+8]
35 call print_int
36 call print_nl
37
38 leave
39 ret

L'exemple ci-dessus n'a que des étiquettes de code globales ; cependant, les étiquettes de donnée globales fonctionnent exactement de la même façon.

V-G. Interfacer de l'assembleur avec du C

Aujourd'hui, très peu de programmes sont écrits complètement en assembleur.

Les compilateurs sont très performants dans la conversion de code de haut niveau en code machine efficace. Comme il est plus facile d'écrire du code dans un langage de haut niveau, ils sont plus populaires. De plus, le code de haut niveau est beaucoup plus portable que l'assembleur !

Lorsque de l'assembleur est utilisé, c'est souvent pour de petites parties du code. Cela peut être fait de deux façons : en appelant des sous-routines assembleur depuis le C ou en incluant de l'assembleur. Inclure de l'assembleur permet au programmeur de placer des instructions assembleur directement dans le code C. Cela peut être très pratique ; cependant, il y a des inconvénients à inclure l'assembleur. Le code assembleur doit être écrit dans le format que le compilateur utilise. Aucun compilateur pour le moment ne supporte le format NASM. Des compilateurs différents demandent des formats différents. Borland et Microsoft demandent le format MASM. DJGPP et gcc sous Linux demandent le format GAS(21). La technique d'appel d'une sous-routine assembleur est beaucoup plus standardisée sur les PC.

Les routines assembleur sont habituellement utilisées avec le C pour les raisons suivantes :

  • un accès direct aux fonctionnalités matérielles de l'ordinateur est nécessaire, car il est difficile ou impossible d'y accéder en C ;
  • la routine doit être la plus rapide possible et le programmeur peut optimiser le code à la main mieux que le compilateur.

La dernière raison n'est plus aussi valide qu'elle l'était. La technologie des compilateurs a été améliorée au fil des ans et les compilateurs génèrent souvent un code très performant (en particulier si les optimisations sont activées).

Les inconvénients des routines assembleur sont une portabilité et une lisibilité réduites.

La plus grande partie des conventions d'appel C a déjà été présentée.

Cependant, il y a quelques fonctionnalités supplémentaires qui doivent être décrites.

V-G-1. Sauvegarder les registres

Tout d'abord, Le C suppose qu'une sous-routine maintient les valeurs des registres suivants : EBX, ESI, EDI, EBP, CS, DS, SS, ES. Cela ne signifie pas que la sous-routine ne les change pas en interne. Cela signifie que si elle change leurs valeurs, elle doit les restaurer avant de revenir. Les valeurs de EBX, ESI et EDI doivent être inchangées, car le C utilise ces registres pour les variables de registre. Habituellement, la pile est utilisée pour sauvegarder les valeurs originales de ces registres.

Le mot-clé register peut être utilisé dans une déclaration de variable C pour suggérer au compilateur d'utiliser un registre pour cette variable plutôt qu'un emplacement mémoire. On appelle ces variables, variables de registre. Les compilateurs modernes le font automatiquement sans qu'il y ait besoin d'une suggestion.

V-G-2. Étiquettes de fonctions

La plupart des compilateurs C ajoutent un caractère underscore(_) au début des noms des fonctions et des variables global/static. Par exemple, à une fonction appelée f sera assignée l'étiquette _f. Donc, s'il s'agit d'une routine assembleur, elle doit être étiquetée _f, pas f. Le compilateur Linux gcc, n'ajoute aucun caractère. Dans un exécutable Linux ELF, on utiliserait simplement l'étiquette f pour la fonction f. Cependant, le gcc de DJGPP ajoute un underscore. Notez que dans le squelette de programme assembleur (Figure 1.7), l'étiquette de la routine principale est _asm_main.

V-G-3. Passer des paramètres

Dans les conventions d'appel C, les arguments d'une fonction sont empilés sur la pile dans l'ordre inverse de celui dans lequel ils apparaissent dans l'appel de la fonction.

Considérons l'expression C suivante : printf("x = %d\n",x); la Figure 4.11 montre comment elle serait compilée (dans le format NASM équivalent).

Image non disponible

La Figure 4.12 montre à quoi ressemble la pile après le prologue de la fonction printf.

Image non disponible

La fonction printf est une des fonctions de la bibliothèque C qui peut prendre n'importe quel nombre d'arguments. Les règles des conventions d'appel C ont été spécialement écrites pour autoriser ce type de fonctions. Comme l'adresse de la chaîne format est empilée en dernier, son emplacement sur la pile sera toujours EBP + 8 quel que soit le nombre de paramètres passés à la fonction. Le code de printf peut alors analyser la chaîne format pour déterminer combien de paramètres ont dû être passés et les récupérer sur la pile.

Bien sûr, s'il y a une erreur, printf("x = %d\n"), le code de printf affichera quand même la valeur double-mot en [EBP + 12]. Cependant, ce ne sera pas la valeur de x !

Il n'est pas nécessaire d'utiliser l'assembleur pour gérer un nombre aléatoire d'arguments en C. L'en-tête stdarg.h définit des macros qui peuvent être utilisées pour l'effectuer de façon portable. Voyez n'importe quel bon livre sur le C pour plus de détails.

V-G-4. Calculer les adresses des variables locales

Trouver l'adresse d'une étiquette définie dans les segments data ou bss est simple. Basiquement, l'éditeur de liens le fait. Cependant, calculer l'adresse d'une variable locale (ou d'un paramètre) sur la pile n'est pas aussi intuitif.

Néanmoins, c'est un besoin très courant lors de l'appel de sous-routines.

Considérons le cas du passage de l'adresse d'une variable (appelons-la x) à une fonction (appelons-la foo). Si x est situé en EBP − 8 sur la pile, on ne peut pas utiliser simplement :

Sélectionnez
mov eax, ebp – 8

Pourquoi ? La valeur que MOV stocke dans EAX doit être calculée par l'assembleur (c'est-à-dire qu'elle doit donner une constante). Cependant, il y a une instruction qui effectue le calcul désiré. Elle est appelée LEA (pour Load Eective Address, Charger l'Adresse Effective). L'extrait suivant calculerait l'adresse de x et la stockerait dans EAX :

Sélectionnez
lea eax, [ebp - 8]

Maintenant, EAX contient l'adresse de x et peut être placé sur la pile lors de l'appel de la fonction foo. Ne vous méprenez pas, au niveau de la syntaxe, c'est comme si cette instruction lisait la donnée en [EBP−8]; cependant, ce n'est pas vrai. L'instruction LEA ne lit jamais la mémoire ! Elle calcule simplement l'adresse qui sera lue par une autre instruction et stocke cette adresse dans son premier opérande registre. Comme elle ne lit pas la mémoire, aucune taille mémoire (p.e. dword) n'est nécessaire ni autorisée.

V-G-5. Retourner des valeurs

Les fonctions C non void retournent une valeur. Les conventions d'appel C spécifient comment cela doit être fait. Les valeurs de retour sont passées via les registres. Tous les types entiers (char, int, enum, etc.) sont retournés dans le registre EAX. S'ils sont plus petits que 32 bits, ils sont étendus à 32 bits lors du stockage dans EAX (la façon dont ils sont étendus dépend du fait qu'ils sont signés ou non). Les valeurs 64 bits sont retournées dans la paire de registres EDX:EAX. Les valeurs de pointeurs sont également stockées dans EAX. Les valeurs en virgule flottante sont stockées dans le registre STP du coprocesseur arithmétique (ce registre est décrit dans le

chapitre sur les nombres en virgule flottante).

V-G-6. Autres conventions d'appel

Les règles ci-dessus décrivent les conventions d'appel C supportées par tous les compilateurs C 80x86. Souvent, les compilateurs supportent également d'autres conventions d'appel. Lorsqu'il y a une interface avec le langage assembleur, il est très important de connaître les conventions utilisées par le compilateur lorsqu'il appelle votre fonction. Habituellement, par défaut, ce sont les conventions d'appel standard qui sont utilisées, cependant, ce n'est pas toujours le cas(22). Les compilateurs qui utilisent plusieurs conventions ont souvent des options de ligne de commande qui peuvent être utilisées pour changer la convention par défaut. Ils fournissent également des extensions à la syntaxe C pour assigner explicitement des conventions d'appel à des fonctions de manière individuelle. Cependant, ces extensions ne sont pas standardisées et peuvent varier d'un compilateur à l'autre.

Le compilateur GCC autorise différentes conventions d'appel. La convention utilisée par une fonction peut être déclarée explicitement en utilisant l'extension __attribute__ . Par exemple, pour déclarer une fonction void qui utilise la convention d'appel standard appelée f qui ne prend qu'un paramètre int, utilisez la syntaxe suivante pour son prototype :

Sélectionnez
void f ( int ) __attribute__((cdecl));

GCC supporte également la convention d'appel standard call . La fonction ci-dessus pourrait être déclarée afin d'utiliser cette convention en remplaçant le cdecl par stdcall. La différence entre stdcall et cdecl est que stdcall impose à la sous-routine de retirer les paramètres de la pile (comme le fait la convention d'appel Pascal). Donc, la convention stdcall ne peut être utilisée que par des fonctions qui prennent un nombre fixe d'arguments (i.e. celles qui ne sont pas comme printf et scanf).

GCC supporte également un attribut supplémentaire appelé regparm qui indique au compilateur d'utiliser les registres pour passer jusqu'à trois arguments entiers à une fonction au lieu d'utiliser la pile. C'est un type d'optimisation courant que beaucoup de compilateurs supportent.

Borland et Microsoft utilisent une syntaxe commune pour déclarer les conventions d'appel. Ils ajoutent les mots-clés __cdecl et __stdcall au C. Ces mots-clés se comportent comme des modicateurs de fonction et apparaissent immédiatement avant le nom de la fonction dans un prototype.

Par exemple, la fonction f ci-dessus serait définie comme suit par Borland et Microsoft :

Sélectionnez
void __cdecl f( int );

Il y a des avantages et des inconvénients à chacune des conventions d'appel.

Le principal avantage de cdecl est qu'elle est simple et très flexible.

Elle peut être utilisée pour n'importe quel type de fonction C et sur n'importe quel compilateur C. Utiliser d'autres conventions peut limiter la portabilité de la sous-routine. Son principal inconvénient est qu'elle peut être plus lente que certaines autres et utilise plus de mémoire (puisque chaque appel de fonction nécessite du code pour retirer les paramètres de la pile).

L'avantage de la convention stdcall est qu'elle utilise moins de mémoire que cdecl. Aucun nettoyage de pile n'est requis après l'instruction CALL. Son principal inconvénient est qu'elle ne peut pas être utilisée avec des fonctions qui ont un nombre variable d'arguments.

L'avantage d'utiliser une convention qui se sert des registres pour passer des paramètres entiers est la rapidité. Le principal inconvénient est que la convention est plus complexe. Certains paramètres peuvent se trouver dans des registres et d'autres sur la pile.

V-G-7. Exemples

Voici un exemple qui montre comment une routine assembleur peut être interfacée avec un programme C (notez que ce programme n'utilise pas le programme assembleur squelette (Figure 1.7) ni le module driver.c).

main5.c
Sélectionnez
1 #include <stdio.h>
2 / prototype de la routine assembleur /
3 void calc_sum( int, int ) __attribute__((cdecl));
4
5 int main( void )
6 {
7 int n, sum;
8
9 printf ("Somme des entiers jusquà ' : ");
10 scanf("%d", &n);
11 calc_sum(n, &sum);
12 printf ("La somme vaut %d\n", sum);
13 return 0;
14 }
sub5.asm
Sélectionnez
1 ; sous-routine _calc_sum
2 ; trouve la somme des entiers de 1 à n
3 ; Paramètres :
4 ; n - jusqu'où faire la somme (en [ebp + 8])
5 ; sump - pointeur vers un entier dans lequel stocker la somme (en [ebp + 12])
6 ; pseudo-code C :
7 ; void calc_sum( int n, int * sump )
8 ; {
9 ; int i, sum = 0;
10 ; for( i=1; i <= n; i++ )
11 ; sum += i;
12 ; *sump = sum;
13 ; }
14
15 segment .text
16 global _calc_sum
17 ;
18 ; variable locale :
19 ; sum en [ebp-4]
20 _calc_sum :
21 enter 4,0 ; Fait de la place pour sum sur la pile
22 push ebx ; IMPORTANT !
23
24 mov dword [ebp-4],0 ; sum = 0
25 dump_stack 1, 2, 4 ; affiche la pile de ebp-8 à ebp+16
26 mov ecx, 1 ; ecx est le i du pseudocode
27 for_loop:
28 cmp ecx, [ebp+8] ; cmp i et n
29 jnle end_for ; si non i <= n, quitter
30
31 add [ebp-4], ecx ; sum += i
32 inc ecx
33 jmp short for_loop
34
35 end_for:
36 mov ebx, [ebp+12] ; ebx = sump
37 mov eax, [ebp-4] ; eax = sum
38 mov [ebx], eax
39
40 pop ebx ; restaure ebx
41 leave
42 ret

Pourquoi la ligne 22 de sub5.asm est si importante ? Parce que les conventions d'appel C imposent que la valeur de EBX ne soit pas modifiée par l'appel de fonction. Si ce n'est pas respecté, il est très probable que le programme ne fonctionne pas correctement.

La ligne 25 montre la façon dont fonctionne la macro dump_stack. Souvenez-vous que le premier paramètre est juste une étiquette numérique et les deuxième et troisième paramètres déterminent respectivement combien de doubles-mots elle doit afficher en dessous et au-dessus de EBP. La Figure 4.13 montre une exécution possible du programme.

Image non disponible

Pour cette capture, on peut voir que l'adresse du dword où stocker la somme est BFFFFB80 (en EBP + 12); le nombre jusqu'auquel additionner est 0000000A (en EBP + 8), l'adresse de retour pour la routine est 08048501 (en EBP + 4), la valeur sauvegardée de EBP est BFFFFB88 (en EBP), la valeur de la variable locale est 0 en (EBP - 4) et pour finir, la valeur sauvegardée de EBX est 4010648C (en EBP - 8).

La fonction calc_sum pourrait être réécrite pour retourner la somme plutôt que d'utiliser un pointeur. Comme la somme est une valeur entière, elle doit être placée dans le registre EAX. La ligne 11 du fichier main5.c deviendrait :

Sélectionnez
sum = calc_sum(n);

De plus, le prototype de calc_sum devrait être altéré. Voici le code assembleur modifié :

sub6.asm
Sélectionnez
1 ; sous-routine _calc_sum
2 ; trouve la somme des entiers de 1 à n
3 ; Paramètres :
4 ; n - jusqu'où faire la somme (en [ebp + 8])
5 ; Valeur de retour :
6 ; valeur de la somme
7 ; pseudo-code C :
8 ; int calc_sum( int n )
9 ; {
10 ; int i, sum = 0;
11 ; for( i=1; i <= n; i++ )
12 ; sum += i;
13 ; return sum;
14 ; }
15 segment .text
16 global _calc_sum
17 ;
18 ; variable locale :
19 ; sum en [ebp-4]
20 _calc_sum :
21 enter 4,0 ; fait de la place pour la somme sur la pile
22
23 mov dword [ebp-4],0 ; sum = 0
24 mov ecx, 1 ; ecx est le i du pseudocode
25 for_loop:
26 cmp ecx, [ebp+8] ; cmp i et n
27 jnle end_for ; si non i <= n, quitter
28
29 add [ebp-4], ecx ; sum += i
30 inc ecx
31 jmp short for_loop
32
33 end_for:
34 mov eax, [ebp-4] ; eax = sum
35
36 leave
37 ret

V-G-8. Appeler des fonctions C depuis l'assembleur

Un des avantages majeurs d'interfacer le C et l'assembleur est que cela permet au code assembleur d'accéder à la grande bibliothèque C et aux fonctions utilisateur. Par exemple, si l'on veut appeler la fonction scanf pour lire un entier depuis le clavier. La Figure 4.14 montre comment le faire.

Image non disponible

Une chose très importante à se rappeler est que scanf suit les conventions d'appel C standard à la lettre. Cela signifie qu'elle préserve les valeurs des registres EBX, ESI et EDI, cependant, les registres EAX, ECX et EDX peuvent être modifiés ! En fait, EAX sera modifié, car il contiendra la valeur de retour de l'appel à scanf. Pour d'autres exemples d'interface entre l'assembleur et le C, observez le code dans asm_io.asm qui a été utilisé pour créer asm_io.obj.

V-H. Sous-programmes réentrants et récursifs

Un sous-programme réentrant remplit les critères suivants :

  • il ne doit pas modifier son code. Dans un langage de haut niveau, cela serait difficile, mais en assembleur, il n'est pas si dur que cela pour un programme de modifier son propre code. Par exemple :
Sélectionnez
mov word [cs :$+7], 5 ; copie 5 dans le mot 7 octets plus loin

add ax, 2 ; l'expression précédente change 2 en 5 !
  • Il ne doit pas modifier de données globales (comme celles qui se trouvent dans les segments data et bss). Toutes les variables sont stockées sur la pile.

Il y a plusieurs avantages à écrire du code réentrant :

  • un sous-programme réentrant peut être appelé récursivement ;
  • un programme réentrant peut être partagé par plusieurs processus ;
  • sur beaucoup de systèmes d'exploitation multitâches, s'il y a plusieurs instances d'un programme en cours, seule une copie du code se trouve en mémoire. Les bibliothèques partagées et les DLL (Dynamic Link Libraries, Bibliothèques de Lien Dynamique) utilisent le même principe ;
  • les sous-programmes réentrants fonctionnent beaucoup mieux dans les programmes multithreadés(23). Windows 9x/NT et la plupart des systèmes d'exploitation de style Unix (Solaris, Linux, etc.) supportent les programmes multithreadés.

V-H-1. Sous-programmes récursifs

Ce type de sous-programmes s'appellent eux-mêmes. La récursivité peut être soit directe soit indirecte. La récursivité directe survient lorsqu'un sous-programme, disons foo, s'appelle lui-même dans le corps de foo. La récursivité indirecte survient lorsqu'un sous-programme ne s'appelle pas directement lui-même, mais via un autre sous-programme qu'il appelle. Par exemple, le sous-programme foo pourrait appeler bar et bar pourrait appeler foo.

Les sous-programmes récursifs doivent avoir une condition de terminaison.

Lorsque cette condition est vraie, il n'y a plus d'appel récursif. Si une routine récursive n'a pas de condition de terminaison ou que la condition n'est jamais remplie, la récursivité ne s'arrêtera jamais (exactement comme une boucle infinie).

La Figure 4.15 montre une fonction qui calcule une factorielle récursivement.

Elle peut être appelée depuis le C avec :

Sélectionnez
x = fact (3); / trouve 3! /
Image non disponible

La Figure 4.16 montre à quoi ressemble la pile au point le plus profond pour l'appel de fonction ci-dessus.

Image non disponible

Les Figures 4.17 et 4.18 montrent un exemple récursif plus compliqué en C et en assembleur, respectivement.

Image non disponible
Image non disponible

Quelle est la sortie pour f(3) ? Notez que l'instruction ENTER crée un nouveau i sur la pile pour chaque appel récursif.

Donc, chaque instance récursive de f a sa propre variable i indépendante.

Définir i comme un double-mot dans le segment data ne fonctionnerait pas pareil.

V-H-2. Révision des types de stockage des variables en C

Le C fournit plusieurs types de stockage des variables :

global : ces variables sont déclarées en dehors de toute fonction et sont stockées à des emplacements mémoire fixes (dans les segments data ou bss) et existent depuis le début du programme jusqu'à la fin. Par défaut, on peut y accéder de n'importe quelle fonction dans le programme, cependant, si elles sont déclarées comme static, seules les fonctions dans le même module peuvent y accéder (i.e. en termes assembleur, l'étiquette est interne, pas externe) ;

static : il s'agit des variables locales d'une fonction qui sont déclarées static (malheureusement, le C utilise le mot-clé static avec deux sens différents !). Ces variables sont également stockées dans des emplacements mémoire fixes (dans data ou bss), mais ne peuvent être accédées directement que dans la fonction où elles sont définies ;

automatic : c'est le type par défaut d'une variable C dénie dans une fonction. Ces variables sont allouées sur la pile lorsque la fonction dans laquelle elles sont définies est appelée et sont désallouées lorsque la fonction revient. Donc, elles n'ont pas d'emplacement mémoire fixe.

register ; ce mot-clé demande au compilateur d'utiliser un registre pour la donnée dans cette variable. Il ne s'agit que d'une requête. Le compilateur n'a pas à l'honorer. Si l'adresse de la variable est utilisée à un endroit quelconque du programme, elle ne sera pas respectée (puisque les registres n'ont pas d'adresse). De plus, seuls les types entiers simples peuvent être des valeurs registres. Les types structures ne peuvent pas l'être : ils ne tiendraient pas dans un registre ! Les compilateurs C placent souvent les variables automatiques normales dans des registres sans aide du programmeur.

volatile : ce mot-clé indique au compilateur que la valeur de la variable peut changer à tout moment. Cela signifie que le compilateur ne peut faire aucune supposition sur le moment où la variable est modifiée. Souvent, un compilateur stocke la valeur d'une variable dans un registre temporairement et utilise le registre à la place de la variable dans une section de code. Il ne peut pas faire ce type d'optimisations avec les variables volatiles. Un exemple courant de variable volatile serait une variable qui peut être modifiée par deux threads d'un programme multithreadé. Considérons le code suivant :

Sélectionnez
1 x = 10;

2 y = 20;

3 z = x;

Si x pouvait être altéré par un autre thread, il est possible que l'autre thread change x entre les lignes 1 et 3, alors z ne vaudrait pas 10.

Cependant, si x n'a pas été déclaré comme volatile, le compilateur peut supposer que x est inchangé et positionner z à 10.

Une autre utilisation de volatile est d'empêcher le compilateur d'utiliser un registre pour une variable.

VI. Tableaux

VI-A. Introduction

Un tableau est un bloc contigu de données en mémoire. Tous les éléments de la liste doivent être du même type et occuper exactement le même nombre d'octets en mémoire. En raison de ces propriétés, les tableaux permettent un accès efficace à une donnée par sa position (ou indice) dans le tableau.

L'adresse de n'importe quel élément peut être calculée en connaissant les trois choses suivantes :

  • l'adresse du premier élément du tableau ;
  • le nombre d'octets de chaque élément ;
  • l'indice de l'élément.

Il est pratique de considérer l'indice du premier élément du tableau comme étant 0 (comme en C). Il est possible d'utiliser d'autres valeurs pour le premier indice, mais cela complique les calculs.

VI-A-1. Définir des tableaux

VI-A-1-a. Définir des tableaux dans les segments data et bss

Pour définir un tableau initialisé dans le segment data, utilisez les directives db, dw, etc. normales. directives. NASM fournit également une directive utile appelée TIMES qui peut être utilisée pour répéter une expression de nombreuses fois sans avoir à la dupliquer à la main. La Figure 5.1 montre plusieurs exemples.

Image non disponible

Pour définir un tableau non initialisé dans le segment bss, utilisez les directives resb, resw, etc. Souvenez-vous que ces directives ont un opérande qui spécifie le nombre d'unités mémoire à réserver. La Figure 5.1 montre également des exemples de ces types de définitions.

VI-A-1-b. Définir des tableaux comme variables locales

Il n'y a pas de manière directe de définir un tableau comme variable locale sur la pile. Comme précédemment, on calcule le nombre total d'octets requis pour toutes les variables locales, y compris les tableaux, et on l'ôte de ESP (soit directement, soit en utilisant l'instruction ENTER). Par exemple, si une fonction a besoin d'une variable caractère, deux entiers double-mot et un tableau de 50 éléments d'un mot, il faudrait 1+2×4+50×2 = 109 octets.

Cependant, le nombre ôté de ESP doit être un multiple de quatre (112 dans ce cas) pour maintenir ESP sur un multiple de double-mot. On peut organiser les variables dans ces 109 octets de plusieurs façons. La Figure 5.2 montre deux manières possibles.

Image non disponible

La partie inutilisée sert à maintenir les doubles-mots sur des adresses multiples de doubles-mots afin d'accélérer les accès mémoire.

VI-A-2. Accéder aux éléments de tableaux

Il n'y a pas d'opérateur [ ] en langage assembleur comme en C. Pour accéder à un élément d'un tableau, son adresse doit être calculée. Considérons les deux définitions de tableau suivantes :

Sélectionnez
array1 db 5, 4, 3, 2, 1 ; tableau d'octets

array2 dw 5, 4, 3, 2, 1 ; tableau de mots

Voici quelques exemples utilisant ces tableaux :

Sélectionnez
1 mov al, [array1] ; al = array1[0]

2 mov al, [array1 + 1] ; al = array1[1]

3 mov [array1 + 3], al ; array1[3] = al

4 mov ax, [array2] ; ax = array2[0]

5 mov ax, [array2 + 2] ; ax = array2[1] (PAS array2[2]!)

6 mov [array2 + 6], ax ; array2[3] = ax

7 mov ax, [array2 + 1] ; ax = ??

À la ligne 5, l'élément 1 du tableau de mots est référencé, pas l'élément 2.

Pourquoi ? Les mots sont des unités de deux octets, donc pour se déplacer à l'élément suivant d'un tableau de mots, on doit se déplacer de deux octets, pas d'un seul.

La ligne 7 lit un octet du premier élément et un octet du suivant. En C, le compilateur regarde le type de pointeur pour déterminer de combien d'octets il doit se déplacer dans une expression utilisant l'arithmétique des pointeurs, afin que le programmeur n'ait pas à le faire.

Cependant, en assembleur, c'est au programmeur de prendre en compte la taille des éléments du tableau lorsqu'il se déplace parmi les éléments.

La Figure 5.3 montre un extrait de code qui additionne tous les éléments de array1 de l'exemple de code précédent.

Image non disponible

À la ligne 7, AX est ajouté à DX. Pourquoi pas AL ? Premièrement, les deux opérandes de l'instruction ADD doivent avoir la même taille. Deuxièmement, il serait facile d'ajouter des octets et d'obtenir une somme qui serait trop grande pour tenir sur un octet. En utilisant DX, la somme peut atteindre 65 535. Cependant, il est important de réaliser que AH est également additionné. C'est pourquoi, AH est positionné à zéro(24) à la ligne 3.

Les Figures 5.4 et 5.5 montrent deux manières alternatives de calculer la somme. Les lignes en italiques remplacent les lignes 6 et 7 de la Figure 5.3.

Image non disponible
Image non disponible

VI-A-3. Adressage indirect plus avancé

Ce n'est pas étonnant, l'adressage indirect est souvent utilisé avec les tableaux. La forme la plus générale d'une référence mémoire indirecte est :

Sélectionnez
[ reg de base + facteur *reg d'index + constante ]

où :

reg de base est un des registres EAX, EBX, ECX, EDX, EBP, ESP, ESI ou EDI ;

facteur est 1, 2, 4 ou 8 (s'il vaut 1, le facteur est omis) ;

reg d'index est un des registres EAX, EBX, ECX, EDX, EBP, ESI, EDI (notez que ESP n'est pas dans la liste) ;

constante est une constante 32 bits. Cela peut être une étiquette (ou une expression d'étiquette).

VI-A-4. Exemple

Voici un exemple qui utilise un tableau et le passe à une fonction. Il utilise le programme array1c.c (dont le listing suit) comme pilote, pas le programme driver.c.

array1.asm
Sélectionnez
1 ?fine ARRAY_SIZE 100
2 ?fine NEW_LINE 10
3
4 segment .data
5 FirstMsg db "10 premiers éléments du tableau", 0
6 Prompt db "Entrez l'indice de l'élément à afficher : ", 0
7 SecondMsg db "L'élément %d vaut %d", NEW_LINE, 0
8 ThirdMsg db "Éléments 20 à 29 du tableau", 0
9 InputFormat db "%d", 0
10
11 segment .bss
12 array resd ARRAY_SIZE
13
14 segment .text
15 extern _puts, _printf, _scanf, _dump_line
16 global _asm_main
17 _asm_main :
18 enter 4,0 ; variable locale dword en EBP – 4
19 push ebx
20 push esi
21
22 ; initialise le tableau à 100, 99, 98, 97...
23
24 mov ecx, ARRAY_SIZE
25 mov ebx, array
26 init_loop:
27 mov [ebx], ecx
28 add ebx, 4
29 loop init_loop
30
31 push dword FirstMsg ; affiche FirstMsg
32 call _puts
33 pop ecx
34
35 push dword 10
36 push dword array
37 call _print_array ; affiche les 10 premiers éléments du tableau
38 add esp, 8
39
40 ; demande à l'utilisateur l'indice de l'élément
41 Prompt_loop:
42 push dword Prompt
43 call _printf
44 pop ecx
45
46 lea eax, [ebp-4] ; eax = adresse du dword local
47 push eax
48 push dword InputFormat
49 call _scanf
50 add esp, 8
51 cmp eax, 1 ; eax = valeur de retour de scanf
52 je InputOK
53
54 call _dump_line ; ignore le reste de la ligne et recommence
55 jmp Prompt_loop ; si la saisie est invalide
56
57 InputOK:
58 mov esi, [ebp-4]
59 push dword [array + 4*esi]
60 push esi
61 push dword SecondMsg ; affiche la valeur de l'élément
62 call _printf
63 add esp, 12
64
65 push dword ThirdMsg ; affiche les éléments 20 à 29
66 call _puts
67 pop ecx
68
69 push dword 10
70 push dword array + 20*4 ; adresse de array[20]
71 call _print_array
72 add esp, 8
73
74 pop esi
75 pop ebx
76 mov eax, 0 ; retour au C
77 leave
78 ret
79
80 ;
81 ; routine _print_array
82 ; Routine appelable depuis le C qui affiche les éléments d'un tableau de doubles mots
83 ; comme des entiers signés.
84 ; Prototype C :
85 ; void print_array( const int * a, int n);
86 ; Paramètres :
87 ; a - pointeur vers le tableau à afficher (en ebp+8 sur la pile)
88 ; n - nombre d'entiers à afficher (en ebp+12 sur la pile)
89
90 segment .data
91 OutputFormat db "%-5d ]", NEW_LINE, 0
92
93 segment .text
94 global _print_array
95 _print_array :
96 enter 0,0
97 push esi
98 push ebx
99
100 xor esi, esi ; esi = 0
101 mov ecx, [ebp+12] ; ecx = n
102 mov ebx, [ebp+8] ; ebx = adresse du tableau
103 print_loop :
104 push ecx ; printf change ecx !
105
106 push dword [ebx + 4*esi] ; empile tableau[esi]
107 push esi
108 push dword OutputFormat
109 call _printf
110 add esp, 12 ; retire les paramètres (laisse ecx !)
111
112 inc esi
113 pop ecx
114 loop print_loop
115
116 pop ebx
117 pop esi
118 leave
119 ret
array1c.c
Sélectionnez
1 #include <stdio.h>
2
3  int asm_main( void );
4 void dump_line( void );
5
6 int main()
7 {
8 int ret_status ;
9 ret_status = asm_main();
10 return ret_status ;
11 }
12
13 /
14 fonction dump_line
15 retire tous les caractères restant sur la ligne courante dans le buffer d'entreé
16 /
17 void dump_line()
18 {
19 int ch;
20
21 while( (ch = getchar()) != EOF && ch != '\n')
22 / null body/ ;
23 }

VI-A-4-a. L'instruction LEA revisitée

L'instruction LEA peut être utilisée dans d'autres cas que le calcul d'adresse.

Elle est assez couramment utilisée pour les calculs rapides. Considérons le code suivant :

Sélectionnez
lea ebx, [4*eax + eax]

Il stocke la valeur de 5×EAX dans EBX. Utiliser LEA dans ce cas est à la fois plus simple et plus rapide que d'utiliser MUL. Cependant, il faut être conscient du fait que l'expression entre crochets doit être une adresse indirecte légale.

Donc, par exemple, cette instruction ne peut pas être utilisée pour multiplier par 6 rapidement.

VI-A-5. Tableaux Multidimensionnels

Les tableaux multidimensionnels ne sont pas vraiment différents de ceux à une dimension dont nous avons déjà parlé. En fait, ils sont représentés en mémoire exactement comme cela, un tableau à une dimension.

VI-A-5-a. Tableaux à Deux Dimensions

Ce n'est pas étonnant, le tableau multidimensionnel le plus simple est celui à deux dimensions. Un tableau à deux dimensions est souvent représenté comme une grille d'éléments. Chaque élément est identifié par une paire d'indices. Par convention, le premier indice est associé à la ligne et le second à la colonne.

Considérons un tableau avec trois lignes et deux colonnes défini de la manière suivante :

Sélectionnez
int a [3][2];

Le compilateur C réserverait de la place pour un tableau d'entiers de 6 (= 2 × 3) et placerait les éléments comme suit :

Image non disponible

Ce que ce tableau tente de montrer et que l'élément référencé comme a[0][0] est stocké au début du tableau de 6 éléments à une dimension. L'élément a[0][1] est stocké à la position suivante (indice 1), etc. Chaque ligne du tableau à deux dimensions est stockée en mémoire de façon contigüe. Le dernier élément d'une ligne est suivi par le premier élément de la suivante. On appelle cela la représentation au niveau ligne (rowwise) du tableau et c'est comme cela qu'un compilateur C/C++ représenterait le tableau.

Comment le compilateur détermine où a[i][j] se trouve dans la représentation au niveau ligne ? Une formule simple calcule l'indice à partir de i et j. La formule dans ce cas est 2i + j. Il n'est pas compliqué de voir comment on obtient cette formule. Chaque ligne fait deux éléments de long, donc le premier élément de la ligne i est à l'emplacement 2i. Puis on obtient l'emplacement de la colonne j en ajoutant j à 2i. Cette analyse montre également comment la formule est généralisée à un tableau de N colonnes : N × i + j.

Notez que la formule ne dépend pas du nombre de lignes.

Pour illustrer, voyons comment gcc compile le code suivant (utilisant le tableau a défini plus haut) :

Sélectionnez
x = a[ i ][ j ];

La Figure 5.6 montre le code assembleur correspondant.

Image non disponible

Donc, le compilateur convertit grossièrement le code en :

Sélectionnez
x = (&a[0][0] + 2i + j );

et en fait, le programmeur pourrait l'écrire de cette manière et obtenir le même résultat.

Il n'y a rien de magique à propos du choix de la représentation niveau ligne du tableau. Une représentation niveau colonne fonctionnerait également :

Image non disponible

Dans la représentation niveau colonne, chaque colonne est stockée de manière contigüe. L'élément [i][j] est stocké à l'emplacement i+3j. D'autres langages (FORTRAN, par exemple) utilisent la représentation niveau colonne.

C'est important lorsque l'on interface du code provenant de multiples langages.

VI-A-5-b. Dimensions supérieures à deux

Pour les dimensions supérieures à deux, la même idée de base est appliquée. Considérons un tableau à trois dimensions :

Sélectionnez
int b [4][3][2];

Ce tableau serait stocké comme s'il était composé de trois tableaux à deux dimensions, chacun de taille [3][2] stockés consécutivement en mémoire. Le tableau ci-dessous montre comment il commence :

Image non disponible

La formule pour calculer la position de b[i][j][k] est 6i + 2j + k. Le 6 est déterminé par la taille des tableaux [3][2]. En général, pour un tableau de dimension a[L][M][N] l'emplacement de l'élément a[i][j][k] sera M ×N ×i+N ×j +k. Notez, là encore, que la dimension L n'apparaît pas dans la formule.

Pour les dimensions plus grandes, le même procédé est généralisé. Pour un tableau à n dimensions de dimension D1 à Dn, l'emplacement d'un élément repéré par les indices i1 à in est donné par la formule :

D2 × D3 · · · × Dn × i1 + D3 × D4 · · · × Dn × i2 + · · · + Dn × in−1 + in

ou pour les fanas de maths, on peut l'écrire de façon plus concise :

Image non disponible

La première dimension, D1, n'apparaît pas dans la formule.

Pour la représentation au niveau colonne, la formule générale serait :

i1 +D1 ×i2 +· · ·+D1 ×D2 ×· · ·×Dn−2 ×in−1 +D1 ×D2 ×· · ·×Dn−1 ×in

ou dans la notation des fanas de maths :

Image non disponible

Dans ce cas, c'est la dernière dimension, Dn, qui n'apparaît pas dans la formule.

C'est là que vous comprenez que l'auteur est un major de physique (ou la référence à FORTRAN vous avait déjà mis sur la voie ?)

VI-A-5-c. Passer des tableaux multidimensionnels comme paramètres en C

La représentation au niveau ligne des tableaux multidimensionnels a un effet direct sur la programmation C. Pour les tableaux à une dimension, la taille du tableau n'est pas nécessaire pour calculer l'emplacement en mémoire de n'importe quel élément. Ce n'est pas vrai pour les tableaux multidimensionnels.

Pour accéder aux éléments de ces tableaux, le compilateur doit connaître toutes les dimensions sauf la première. Cela saute aux yeux lorsque l'on observe le prototype d'une fonction qui prend un tableau multidimensionnel comme paramètre. Ce qui suit ne compilera pas :

Sélectionnez
void f ( int a [ ][ ] ); / pas d' information sur la dimension /

Cependant, ce qui suit compile :

Sélectionnez
void f ( int a [ ][2] );

Tout tableau à deux dimensions à deux colonnes peut être passé à cette fonction. La première dimension n'est pas nécessaire(25).

Ne confondez pas avec une fonction ayant ce prototype :

Sélectionnez
void f ( int a [ ] );

Cela définit un tableau à une dimension de pointeurs sur des entiers (qui peut à son tour être utilisé pour créer un tableau de tableaux qui se comportent plus comme un tableau à deux dimensions).

Pour les tableaux de dimensions supérieures, toutes les dimensions sauf la première doivent être données pour les paramètres. Par exemple, un paramètre tableau à quatre dimensions peut être passé de la façon suivante :

Sélectionnez
void f ( int a [ ][4][3][2] );

VI-B. Instructions de tableaux/chaînes

La famille des processeurs 80x86 fournit plusieurs instructions conçues pour travailler avec les tableaux. Ces instructions sont appelées instructions de chaînes. Elles utilisent les registres d'index (ESI et EDI) pour effectuer une opération puis incrémentent ou décrémentent automatiquement l'un des registres d'index ou les deux. Le drapeau de direction (DF) dans le registre FLAGS détermine si les registres d'index sont incrémentés ou décrémentés.

Il y a deux instructions qui modifient le drapeau de direction :

CLD éteint le drapeau de direction. Les registres d'index sont alors incrémentés ;

STD allume le drapeau de direction. Les registres d'index sont alors décrémentés.

Une erreur très courante dans la programmation 80x86 est d'oublier de positionner le drapeau de direction. Cela conduit souvent à un code qui fonctionne la plupart du temps (lorsque le drapeau de direction est dans la position désirée), mais ne fonctionne pas tout le temps.

VI-B-1. Lire et écrire en mémoire

Les instructions de chaînes les plus simples lisent ou écrivent en mémoire, ou les deux. Elles peuvent lire ou écrire un octet, un mot ou un double-mot à la fois. La Figure 5.7 montre ces instructions avec une brève description de ce qu'elles font en pseudo-code.

Image non disponible

Il y a plusieurs choses à noter ici. Tout d'abord, ESI est utilisé pour lire et EDI pour écrire. C'est facile à retenir si l'on se souvient que SI signifie Source Index (Indice Source) et DI Destination Index (Indice de Destination). Ensuite, notez que le registre qui contient la donnée est fixe (AL, AX ou EAX). Enfin, notez que les instructions de stockage utilisent ES pour déterminer le segment dans lequel écrire, pas DS. En programmation en mode protégé, ce n'est habituellement pas un problème, puisqu'il n'y a qu'un segment de données et ES est automatiquement initialisé pour y faire référence (tout comme DS). Cependant, en programmation en mode réel, il est très important pour le programmeur d'initialiser ES avec la valeur de sélecteur de segment correcte(26). La Figure 5.8 montre un exemple de l'utilisation de ces instructions qui copie un tableau dans un autre.

Image non disponible

La combinaison des instructions LODSx et STOSx (comme aux lignes 13 et 14 de la Figure 5.8) est très courante. En fait, cette combinaison peut être effectuée par une seule instruction de chaînes MOVSx. La Figure 5.9 décrit les opérations effectuées par cette instruction.

Image non disponible

Les lignes 13 et 14 de la Figure 5.8 pourraient être remplacées par une seule instruction MOVSD avec le même résultat. La seule différence serait que le registre EAX ne serait pas utilisé du tout dans la boucle.

VI-B-2. Le préfixe d'instruction REP

La famille 80x86 fournit un préfixe d'instruction spécial(27) appelé REP qui peut être utilisé avec les instructions de chaînes présentées ci-dessus. Ce préfixe indique au processeur de répéter l'instruction de chaînes qui suit un nombre précis de fois. Le registre ECX est utilisé pour compter les itérations (exactement comme pour l'instruction LOOP). En utilisant le préfixe REP, la boucle de la Figure 5.8 (lignes 12 à 15) pourrait être remplacée par une seule ligne :

Sélectionnez
rep movsd

La Figure 5.10 montre un autre exemple qui met à zéro le contenu d'un tableau.

Image non disponible

VI-B-3. Instructions de comparaison de chaînes

La Figure 5.11 montre plusieurs nouvelles instructions de chaînes qui peuvent être utilisées pour comparer des données en mémoire entre elles ou avec des registres. Elles sont utiles pour comparer des tableaux ou effectuer des recherches. Elles positionnent le registre FLAGS exactement comme l'instruction CMP. Les instructions CMPSx comparent les emplacements mémoire correspondants et les instructions SCASx scannent des emplacements mémoire pour une valeur particulière.

La Figure 5.12 montre un court extrait de code qui recherche le nombre 12 dans un tableau de doubles-mots.

Image non disponible

L'instruction SCASD à la ligne 10 ajoute toujours 4 à EDI, même si la valeur recherchée est trouvée. Donc, si l'on veut trouver l'adresse du 12 dans le tableau, il est nécessaire de soustraire 4 de EDI (comme le fait la ligne 16).

VI-B-4. Les préfixes d'instruction REPx

Il y a plusieurs autres préfixes d'instruction du même genre que REP qui peuvent être utilisés avec les instructions de comparaison de chaînes.

La Figure 5.13 montre les deux nouveaux préfixes et décrit ce qu'ils font.

Image non disponible

REPE et REPZ sont simplement des synonymes pour le même préfixe (comme le sont REPNE et REPNZ). Si l'instruction de comparaison de chaînes répétée stoppe à cause du résultat de la comparaison, le ou les registres d'index sont quand même incrémentés et ECX décrémenté, cependant, le registre FLAGS contient toujours l'état qu'il avait à la fin de la répétition. Donc, il est possible d'utiliser le drapeau Z pour déterminer si la comparaison s'est arrêtée à cause de son résultat ou si c'est parce que ECX a atteint zéro.

Pourquoi ne peut-on pas simplement regarder si ECX est à zéro après la comparaison répétée ?

La Figure 5.14 montre un extrait de code qui détermine si deux blocs de mémoire sont égaux.

Image non disponible

Le JE de la ligne 7 de l'exemple vérifie le résultat de l'instruction précédente. Si la comparaison s'est arrêtée parce qu'elle a trouvé deux octets différents, le drapeau Z sera toujours éteint et aucun branchement n'est effectué, cependant, si la comparaison s'est arrêtée parce que ECX a atteint zéro, le drapeau Z sera toujours allumé et le code se branche à l'étiquette equal.

VI-B-5. Exemple

Cette section contient un fichier source assembleur avec plusieurs fonctions qui implémentent des opérations sur les tableaux en utilisant des instructions de chaînes. Beaucoup de ces fonctions font doublons avec des fonctions familières de la bibliothèque C.

memory.asm
Sélectionnez
1 global _asm_copy, _asm_find, _asm_strlen, _asm_strcpy
2
3 segment .text
4 ; fonction _asm_copy
5 ; copie deux blocs de mémoire
6 ; prototype C
7 ; void asm_copy( void * dest, const void * src, unsigned sz);
8 ; paramètres :
9 ; dest - pointeur sur le tampon vers lequel copier
10 ; src - pointeur sur le tampon depuis lequel copier
11 ; sz - nombre d'octets à copier
12
13 ; ci-dessous, quelques symboles utiles sont définis
14
15 ?fine dest [ebp+8]
16 ?fine src [ebp+12]
17 ?fine sz [ebp+16]
18 _asm_copy :
19 enter 0, 0
20 push esi
21 push edi
22
23 mov esi, src ; esi = adresse du tampon depuis lequel copier
24 mov edi, dest ; edi = adresse du tampon vers lequel copier
25 mov ecx, sz ; ecx = nombre d'octets à copier
26
27 cld ; éteint le drapeau de direction
28 rep movsb ; exécute movsb ECX fois
29
30 pop edi
31 pop esi
32 leave
33 ret
34
35
36 ; fonction _asm_find
37 ; recherche un octet donné en mémoire
38 ; void * asm_find( const void * src, char target, unsigned sz);
39 ; paramètres :
40 ; src - pointeur sur le tampon dans lequel chercher
41 ; target - octet à rechercher
42 ; sz - nombre d'octets dans le tampon
43 ; valeur de retour :
44 ; si l'élément recherché est trouvé, un pointeur vers sa première occurrence dans le tampon
45 ; est retourné
46 ; sinon, NULL est retourné
47 ; NOTE : l'élément à rechercher est un octet, mais il est empilé comme une valeur dword.
48 ; La valeur de l'octet est stockée dans les 8 bits de poids faible.
49 ;
50 ?fine src [ebp+8]
51 ?fine target [ebp+12]
52 ?fine sz [ebp+16]
53
54 _asm_find :
55 enter 0,0
56 push edi
57
58 mov eax, target ; al a la valeur recherchée
59 mov edi, src
60 mov ecx, sz
61 cld
62
63 repne scasb ; scanne jusqu'à ce que ECX == 0 ou [ES:EDI] == AL
64
65 je found_it ; si le drapeau zéro est allumé, on a trouvé
66 mov eax, 0 ; si pas trouvé, retourner un pointeur NULL
67 jmp short quit
68 found_it :
69 mov eax, edi
70 dec eax ; si trouvé retourner (DI - 1)
71 quit :
72 pop edi
73 leave
74 ret
75
76
77 ; fonction _asm_strlen
78 ; retourne la taille d'une chaîne
79 ; unsigned asm_strlen( const char * );
80 ; paramètre :
81 ; src - pointeur sur la chaîne
82 ; valeur de retour :
83 ; nombre de caractères dans la chaîne (sans compter le 0 terminal) (dans EAX)
84
85 ?fine src [ebp + 8]
86 _asm_strlen :
87 enter 0,0
88 push edi
89
90 mov edi, src ; edi = pointeur sur la chaîne
91 mov ecx, 0FFFFFFFFh ; utilise la plus grande valeur possible de ECX
92 xor al,al ; al = 0
93 cld
94
95 repnz scasb ; recherche le 0 terminal
96
97 ;
98 ; repnz ira un cran trop loin, donc la longueur vaut FFFFFFFE - ECX,
99 ; pas FFFFFFFF - ECX
100 ;
101 mov eax,0FFFFFFFEh
102 sub eax, ecx ; longueur = 0FFFFFFFEh - ecx
103
104 pop edi
105 leave
106 ret
107
108 ; fonction _asm_strcpy
109 ; copie une chaîne
110 ; void asm_strcpy( char * dest, const char * src);
111 ; paramètres :
112 ; dest - pointeur sur la chaine vers laquelle copier
113 ; src - pointeur sur la chaîne depuis laquelle copier
114 ;
115 ?fine dest [ebp + 8]
116 ?fine src [ebp + 12]
117 _asm_strcpy :
118 enter 0,0
119 push esi
120 push edi
121
122 mov edi, dest
123 mov esi, src
124 cld
125 cpy_loop:
126 lodsb ; charge AL & incrémente SI
127 stosb ; stocke AL & incrémente DI
128 or al, al ; positionne les drapeaux de condition
129 jnz cpy_loop ; si l'on n’est pas après le 0 terminal, on continue
130
131 pop edi
132 pop esi
133 leave
134 ret
memex.c
Sélectionnez
1 #include <stdio.h>
2
3 #dene STR_SIZE 30
4 / prototypes /
5
6 void asm_copy( void , const void , unsigned ) __attribute__((cdecl));
7 void asm_nd( const void ,
8 char target , unsigned ) __attribute__((cdecl));
9 unsigned asm_strlen( const char ) __attribute__((cdecl));
10 void asm_strcpy( char , const char ) __attribute__((cdecl));
11
12 int main()
13 {
14 char st1 [STR_SIZE] = "chaine test";
15 char st2 [STR_SIZE];
16 char st ;
17 char ch;
18
19 asm_copy(st2, st1, STR_SIZE); / copie les 30 caractères de la chaine /
20 printf ("%s\n", st2);
21
22 printf ("Entrez un è caractre : " ); / recherche un octet dans la îchane /
23 scanf("%c%[^\n]", &ch);
24 st = asm_nd(st2, ch, STR_SIZE);
25 if ( st )
26 printf ("éTrouv : %s\n", st );
27 else
28 printf ("Pas étrouv\n");
29
30 st1 [0] = 0;
31 printf ("Entrez une îchane :");
32 scanf("%s", st1);
33 printf ("longueur = %u\n", asm_strlen(st1));
34
35 asm_strcpy( st2, st1 ); / copie des édonnes dans la îchane /
36 printf ("%s\n", st2 );
37
38 return 0;
39 }

VII. Virgule flottante

VII-A. Représentation en virgule flottante

VII-A-1. Nombres binaires non entiers

Lorsque nous avons parlé des systèmes numériques dans le premier chapitre, nous n'avons abordé que les nombres entiers. Évidemment, il est possible de représenter des nombres non entiers dans des bases autres que le décimal.

En décimal, les chiffres à droite du symbole décimal sont associés à des puissances

négatives de 10 :

0, 123 = 1 × 10-1 + 2 × 10-2 + 3 × 10-3

Ce n'est pas étonnant, les nombres binaires fonctionnent de la même façon :

0, 1012 = 1 × 2-1 + 0 × 2-2 + 1 × 2-3 = 0, 625

Cette idée peut être associée avec les méthodes appliquées aux entiers dans le Chapitre II pour convertir un nombre quelconque :

110, 0112 = 4 + 2 + 0, 25 + 0, 125 = 6, 375

Convertir du binaire vers le décimal n'est pas très difficile non plus. Divisez le nombre en deux parties : entière et décimale. Convertissez la partie entière en binaire en utilisant les méthodes du Chapitre II, la partie décimale est convertie en utilisant la méthode décrite ci-dessous.

Considérons une partie décimale binaire dont les bits sont notés a, b, c… Le nombre en binaire ressemble alors à :

0, abcdef…

Multipliez le nombre par deux. La représentation du nouveau nombre sera :

a, bcdef…

Notez que le premier bit est maintenant en tête. Remplacez le a par 0 pour obtenir :

0, bcdef…

et multipliez à nouveau par deux, vous obtenez :

b, cdef…

Maintenant le deuxième bit (b) est à la première place. Cette procédure peut être répétée jusqu'à ce qu'autant de bits que nécessaire soient trouvés.

La Figure 6.1 montre un exemple réel qui converti 0,5625 en binaire.

Image non disponible

La méthode s'arrête lorsque la partie décimale arrive à zéro.

Prenons un autre exemple, considérons la conversion de 23,85 en binaire, il est facile de convertir la partie entière (23 = 101112), mais qu'en est-il de la partie décimale (0, 85) ?

La Figure 6.2 montre le début de ce calcul.

Image non disponible
Image non disponible

Une conséquence importante du calcul ci-dessus est que 23,85 ne peut pas être représenté exactement en binaire en utilisant un nombre fini de bits (tout comme 1/3 ne peut pas être représenté en décimal avec un nombre fini de chiffres). Comme le montre ce chapitre, les variables float et double en C sont stockées en binaire. Donc, les valeurs comme 23,85 ne peuvent pas être stockées exactement dans ce genre de variables. Seule une approximation de 23,85 peut être stockée.

Pour simplifier le matériel, les nombres en virgule flottante sont stockés dans un format standard. Ce format utilise la notation scientifique (mais en binaire, en utilisant des puissances de 2, pas de 10). Par exemple, 23,85 ou 10111, 11011001100110…2 serait stocké sous la forme :

1, 011111011001100110… × 2100

(où l'exposant (100) est en binaire). Un nombre en virgule flottante normalisé a la forme :

1, ssssssssssssssss × 2eeeeeee

où 1, sssssssssssss est la mantisse et eeeeeeee est l'exposant.

VII-A-2. Représentation en virgule flottante IEEE

L'IEEE (Institute of Electrical and Electronic Engineers) est une organisation internationale qui a conçu des formats binaires spécifiques pour stocker les nombres en virgule flottante. Ce format est utilisé sur la plupart des ordinateurs (mais pas tous !) fabriqués de nos jours. Souvent, il est supporté par le matériel de l'ordinateur lui-même. Par exemple, les coprocesseurs numériques (ou arithmétiques) d'Intel (qui sont intégrés à tous leurs processeurs depuis le Pentium) l'utilisent. L'IEEE définit deux formats différents avec des précisions différentes : simple et double précision. La simple précision est utilisée pour les variables float en C et la double précision pour les variables double.

Le coprocesseur arithmétique d'Intel utilise également une troisième précision plus élevée appelée précision étendue. En fait toutes les données dans le coprocesseur sont dans ce format. Lorsqu'elles sont stockées en mémoire depuis le coprocesseur, elles sont converties soit en simple, soit en double précision automatiquement(28). La précision étendue utilise un format général légèrement différent des formats float et double de l'IEEE, nous n'en parlerons donc pas ici.

VII-A-2-a. Simple précision IEEE

La virgule flottante simple précision utilise 32 bits pour encoder le nombre.

Elle est généralement précise jusqu'à 7 chiffres significatifs. Les nombres en virgule flottante sont généralement stockés dans un format beaucoup plus compliqué que celui des entiers.

La Figure 6.3 montre le format de base d'un nombre en simple précision IEEE.

Image non disponible

Ce format a plusieurs inconvénients. Les nombres en virgule flottante n'utilisent pas la représentation en complément à 2 pour les nombres négatifs. Ils utilisent une représentation en grandeur signée. Le bit 31 détermine le signe du nombre.

L'exposant binaire n'est pas stocké directement. À la place, la somme de l'exposant et de 7F est stockée dans les bits 23 à 30, cet exposant décalé est toujours positif ou nul.

La partie décimale suppose une mantisse normalisée (de la forme 1, sssssssss).

Comme le premier bit est toujours un 1, le 1 de gauche n'est pas stocké ! Cela permet le stockage d'un bit additionnel à la fin et augmente donc légèrement la précision. Cette idée est appelée représentation en 1 masqué.

Il faut toujours garder à l'esprit que les octets 41 BE CC CD peuvent être interprétés de façons différentes selon ce qu'en fait le programme ! Vus comme un nombre en virgule flottante en simple précision, ils représentent 23,850000381, mais vus comme un entier double-mot, ils représentent 1,103,023,309 ! Le processeur ne sait pas quelle est la bonne interprétation !

Comment serait stocké 23,85 ? Tout d'abord, il est positif, donc le bit de signe est à 0. Ensuite, l'exposant réel est 4, donc l'exposant décalé est

7F + 4 = 8316.

Enfin, la fraction vaut 01111101100110011001100 (souvenez-vous que le 1 de tête est masqué). En les mettant bout à bout (pour clarifier les différentes sections du format en virgule flottante, le bit de signe et la fraction ont été soulignés et les bits ont été regroupés en groupes de 4 bits) :

Image non disponible

Cela ne fait pas exactement 23,85 (puisqu'il s'agit d'un binaire répétitif). Si l'on convertit le chiffre ci-dessus en décimal, on constate qu'il vaut approximativement 23,849998474, ce nombre est très proche de 23,85, mais n'est pas exactement le même. En réalité, en C, 23,85 ne serait pas représenté exactement comme ci-dessus. Comme le bit le plus à gauche qui a été supprimé de la représentation exacte valait 1, le dernier bit est arrondi à 1, donc 23,85 serait représenté par 41 BE CC CD en hexa en utilisant la simple précision. En décimal, ce chiffre vaut 23,850000381 ce qui est une meilleure approximation de 23,85.

Comment serait représenté -23,85 ? Il suffit de changer le bit de signe :

C1 BE CC CD. Ne prenez pas le complément à 2 !

Certaines combinaisons de e et f ont une signification spéciale pour les flottants IEEE. Le Tableau 6.1 décrit ces valeurs.

Image non disponible

Un infini est produit par un dépassement de capacité ou une division par zéro. Un résultat indéfini

est produit par une opération invalide comme rechercher la racine carrée d'un nombre négatif, additionner deux infinis, etc.

Les nombres en simple précision normalisés peuvent prendre des valeurs de

Image non disponible

VII-A-2-b. Nombres dénormalisés

Les nombres dénormalisés peuvent être utilisés pour représenter des nombres avec des grandeurs trop petites à normaliser (i.e. inférieures à 1, 0 × 2-126).

Par exemple, considérons le nombre Image non disponible

Dans la forme normalisée, l'exposant est trop petit. Par contre, il peut être représenté sous une forme non normalisée : 0, 010012 × 2-127. Pour stocker ce nombre, l'exposant décalé est positionné à 0 (voir Tableau 6.1) et la partie décimale est la mantisse complète du nombre écrite comme un produit avec 2-127 (i.e. tous les bits sont stockés, y compris le 1 à gauche du point décimal). La représentation de 1, 001 × 2-129 est donc :

Image non disponible

VII-A-2-c. Double précision IEEE

La double précision IEEE utilise 64 bits pour représenter les nombres et est précise jusqu'à environ 15 chiffres significatifs. Comme le montre la Figure 6.4, le format de base est très similaire à celui de la simple précision.

Image non disponible

Plus de bits sont utilisés pour l'exposant décalé (11) et la partie décimale (52).

La plage de valeurs plus grande pour l'exposant décalé a deux conséquences. La première est qu'il est calculé en faisant la somme de l'exposant réel et de 3FF (1023) (pas 7F comme pour la simple précision). Deuxièmement, une grande plage d'exposants réels (et donc une plus grande plage de grandeurs) est disponible. Les grandeurs en double-précision peuvent prendre des valeurs entre environ 13-308 et 10308 .

C'est la plus grande taille du champ réservé à la partie décimale qui est reponsable de l'augmentation du nombre de chiffres significatifs pour les valeurs doubles.

Pour illustrer cela, reprenons 23,85. L'exposant décalé sera 4+3FF = 403 en hexa. Donc, la représentation double sera :

Image non disponible

ou 40 37 D9 99 99 99 99 9A en hexa. Si l'on reconvertit ce nombre en décimal, on trouve 23,8500000000000014 (il y a 12 zéros !) ce qui est une approximation de 23,85 nettement meilleure.

La double précision a les mêmes valeurs spéciales que la simple précision(29).

Les nombres dénormalisés sont également très similaires. La seule différence principale est que les nombres doubles dénormalisés utilisent 2-1023 au lieu de 2-127.

VII-B. Arithmétique en virgule flottante

L'arithmétique en virgule flottante sur un ordinateur est différente de celle des mathématiques. En mathématiques, tous les nombres peuvent être considérés comme exacts. Comme nous l'avons montré dans la section précédente, sur un ordinateur beaucoup de nombres ne peuvent pas être représentés exactement avec un nombre fini de bits. Tous les calculs sont effectués avec une précision limitée. Dans les exemples de cette section, des nombres avec une mantisse de 8 bits seront utilisés pour plus de simplicité.

VII-B-1. Addition

Pour additionner deux nombres en virgule flottante, les exposants doivent être égaux. S'ils ne le sont pas déjà, alors, il faut les rendre égaux en décalant la mantisse du nombre ayant le plus petit exposant. Par exemple, considérons

10, 375 + 6, 34375 = 16, 71875 ou en binaire :

Image non disponible

Ces deux nombres n'ont pas le même exposant, donc on décale la mantisse pour rendre les exposants égaux, puis on effectue l'addition :

Image non disponible

Notez que le décalage de 1, 1001011 × 22 supprime le 1 de tête et arrondi plus tard le résultat en 0, 1100110 × 23. Le résultat de l'addition, 10, 0001100 × 23 ou 1, 00001100 × 24), est égal à

10000,1102 ou 16,75, ce n'est pas égal à la réponse exacte (16,71875) ! Il ne s'agit que d'une approximation due aux erreurs d'arrondi du processus d'addition.

Il est important de réaliser que l'arithmétique en virgule flottante sur un ordinateur (ou une calculatrice) est toujours une approximation. Les lois des mathématiques ne fonctionnent pas toujours avec les nombres en virgule flottante sur un ordinateur. Les mathématiques supposent une précision infinie qu'aucun ordinateur ne peut atteindre. Par exemple, les mathématiques nous apprennent que (a + b) − b = a, cependant, ce n'est pas forcément exactement vrai sur un ordinateur !

VII-B-2. Soustraction

La soustraction fonctionne d'une façon similaire à l'addition et souffre des mêmes problèmes. Par exemple, considérons 16, 75 − 15, 9375 = 0, 8125 :

Image non disponible

Décaler 1, 1111111 × 23 donne (en arrondissant) 1, 0000000 × 24

Image non disponible

VII-B-3. Multiplication et division

Pour la multiplication, les mantisses sont multiplées et les exposants sont additionnés. Considérons 10, 375 × 2, 5 = 25, 9375 :

Image non disponible

Bien sûr, le résultat réel serait arrondi à 8 bits pour donner :

1, 1010000 × 24 = 11010, 0002 = 26

La division est plus compliquée, mais souffre des mêmes problèmes avec des erreurs d'arrondi.

VII-B-4. Conséquences sur la programmation

Le point essentiel de cette section est que les calculs en virgule flottante ne sont pas exacts. Le programmeur doit en être conscient. Une erreur courante que les programmeurs font avec les nombres en virgule flottante est de les comparer en supposant qu'un calcul est exact. Par exemple, considérons une fonction appelée f(x) qui effectue un calcul complexe et un programme qui essaie de trouver la racine de la fonction(30). On peut être tenté d'utiliser l'expression suivante pour vérifier si x est une racine :

Sélectionnez
if ( f(x) == 0.0 )

Mais, que se passe-t-il si f(x) retourne 1×10-30 ? Cela signifie très probablement que x est une très bonne approximation d'une racine réelle, cependant, l'égalité ne sera pas vérifiée. Il n'y a peut-être aucune valeur en virgule flottante IEEE de x qui renvoie exactement zéro, en raison des erreurs d'arrondi dans f(x).

Une meilleure méthode serait d'utiliser :

Sélectionnez
if ( fabs(f(x)) < EPS )

où EPS est une macro définissant une très petite valeur positive (du genre 1 x 10-10). Cette expression est vraie dès que f(x) est très proche de zéro.

En général, pour comparer une valeur en virgule flottante (disons x) à une autre (y), on utilise :

Sélectionnez
if ( fabs(x &#8722; y)/fabs(y) < EPS )

VII-C. Le coprocesseur arithmétique

VII-C-1. Matériel

Les premiers processeurs Intel n'avaient pas de support matériel pour les opérations en virgule flottante. Cela ne signifie pas qu'ils ne pouvaient pas effectuer de telles opérations. Cela signifie seulement qu'elles devaient être réalisées par des procédures composées de beaucoup d'instructions qui n'étaient pas en virgule flottante. Pour ces systèmes, Intel fournissait une puce appelée coprocesseur mathématique. Un coprocesseur mathématique a des instructions machine qui effectuent beaucoup d'opérations en virgule flottante beaucoup plus rapidement qu'en utilisant une procédure logicielle (sur les premiers processeurs, au moins 10 fois plus vite !). Le coprocesseur pour le 8086/8088 s'appelait le 8087, pour le 80286, il y avait un 80287 et pour le 80386, un 80387, le processeur 80486DX intégrait le coprocesseur mathématique dans le 80486 lui-même(31). Depuis le Pentium, toutes les générations de processeurs 80x86 ont un coprocesseur mathématique intégré. Cependant, on le programme toujours comme s'il s'agissait d'une unité séparée. Même les systèmes plus anciens sans coprocesseur peuvent installer un logiciel qui émule un coprocesseur mathématique. Cet émulateur est automatiquement activé lorsqu'un programme exécute une instruction du coprocesseur et lance la procédure logicielle qui produit le même résultat que celui qu'aurait donné le coprocesseur (bien que cela soit plus lent, bien sûr).

Le coprocesseur arithmétique a huit registres de virgule flottante. Chaque registre contient 80 bits de données. Les nombres en virgule flottante sont toujours stockés sous la forme de nombres 80 bits en précision étendue dans ces registres. Les registres sont appelés ST0, ST1, ST2… ST7. Les registres en virgule flottante sont utilisés différemment des registres entiers du processeur principal. Ils sont organisés comme une pile. Souvenez-vous qu'une pile est une liste Last-In First-Out (LIFO, dernier entré, premier sorti). ST0 fait toujours référence à la valeur au sommet de la pile. Tous les nouveaux nombres sont ajoutés au sommet de la pile. Les nombres existants sont décalés vers le bas pour faire de la place au nouveau.

Il y a également un registre de statut dans le coprocesseur arithmétique.

Il a plusieurs drapeaux. Seuls les 4 drapeaux utilisés pour les comparaisons seront traités : C0, C1, C2 et C3. Leur utilisation est traitée plus tard.

VII-C-2. Instructions

Pour faciliter la distinction entre les instructions du processeur normal et celles du coprocesseur, tous les mnémoniques du coprocesseur commencent par un F.

VII-C-2-a. Chargement et stockage

Il y a plusieurs instructions qui chargent des données au sommet de la pile du coprocesseur :

FLD source charge un nombre en virgule flottante depuis la mémoire vers

le sommet de la pile. La source peut être un nombre en simple,

double ou précision étendue ou un registre du coprocesseur.

FILD source lit un entier depuis la mémoire, le convertit en flottant et stocke le résultat au sommet de la pile. La source peut être un mot, un double-mot ou un quadruple-mot.

FLD1 Stocke un 1 au sommet de la pile.

FLDZ Stocke un zéro au sommet de la pile.

Il y a également plusieurs instructions qui déplacent les données depuis la pile vers la mémoire. Certaines de ces instructions retirent le nombre de la pile en même temps qu'elles le déplacent.

FST dest stocke le sommet de la pile (ST0) en mémoire. La destination peut être un nombre en simple ou double précision ou un registre du coprocesseur.

FSTP dest stocke le sommet de la pile en mémoire, exactement comme FST ; cependant, une fois le nombre stocké, sa valeur est retirée de la pile. La destination peut être un nombre en simple, double ou précision étendue ou un registre du coprocesseur.

FIST dest stocke la valeur au sommet de la pile convertie en entier en mémoire. La destination peut être un mot ou un double-mot. La pile en elle-même reste inchangée. La façon dont est converti le nombre en entier dépend de certains bits dans le mot de contrôle du coprocesseur. Il s'agit d'un registre d'un mot spécial (pas en virgule flottante) qui contrôle le fonctionnement du coprocesseur. Par défaut, le mot de contrôle est initialisé afin qu'il arrondisse à l'entier le plus proche lorsqu'il convertit vers un entier. Cependant, les instructions FSTCW (Store Control Word, stocker le mot de contrôle) et FLDCW (Load Control Word, charger le mot de contrôle) peuvent être utilisées pour changer ce comportement.

FISTP dest identique à FIST sauf en deux points. Le sommet de la pile est supprimé et la destination peut également être un quadruple-mot.

Il y a deux autres instructions qui peuvent placer ou retirer des données de la pile.

FXCH STn échange les valeurs de ST0 et STn sur la pile (où n est un numéro de registre entre 1 et 7).

FFREE STn libère un registre sur la pile en le marquant comme inutilisé ou vide.

VII-C-2-b. Addition et soustraction

Chacune des instructions d'addition calcule la somme de ST0 et d'un autre opérande. Le résultat est toujours stocké dans un registre du coprocesseur.

FADD source ST0 += source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FADD dest, ST0 dest += ST0. La destination peut être n'importe quel registre du coprocesseur.

FADDP dest ou FADDP dest, STO dest += ST0 puis ST0 est retiré de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIADD source ST0 += (float) source . Ajoute un entier à ST0. La source doit être un mot ou un double-mot en mémoire.

Il y a deux fois plus d'instructions pour la soustraction que pour l'addition, car l'ordre des opérandes est important pour la soustraction (i.e. a+b = b+a, mais a−b ≠ b−a !). Pour chaque instruction, il y a un miroir qui effectue la soustraction dans l'ordre inverse. Ces instructions inverses se finissent toutes soit par R soit par RP. La Figure 6.5 montre un court extrait de code qui ajoute les éléments d'un tableau de doubles.

Image non disponible

Au niveau des lignes 10 et 13, il faut spécifier la taille de l'opérande mémoire. Sinon l'assembleur ne saurait pas si l'opérande mémoire est un float (dword) ou un double (qword).

FSUB source ST0 -= source. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FSUBR source ST0 = source - ST0. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FSUB dest, ST0 dest -= ST0. La destination peut être n'importe quel registre du coprocesseur.

FSUBR dest, ST0 dest = ST0 - dest . La destination peut être n'importe quel registre du coprocesseur.

FSUBP dest ou FSUBP dest, STO dest -= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FSUBRP dest ou FSUBRP dest, ST0 dest = ST0 - dest puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FISUB source ST0 -= (float) source . Soustrait un entier de ST0. La source doit être un mot ou un double mot en mémoire.

FISUBR source ST0 = (float) source - ST0. Soustrait ST0 d'un entier. La source doit être un mot ou un double-mot en mémoire.

VII-C-2-c. Multiplication et division

Les instructions de multiplication sont totalement analogues à celles d'addition.

FMUL source ST0 *= source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FMUL dest, ST0 dest *= ST0. La destination peut être n'importe quel registre du coprocesseur.

FMULP dest ou FMULP dest, ST0 dest *= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIMUL source ST0 *= (float) source. Multiplie un entier avec ST0. La source peut être un mot ou un double-mot en mémoire.

Ce n'est pas étonnant, les instructions de division sont analogues à celles de soustraction. La division par zéro donne l'infini.

FDIV source ST0 /= source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FDIVR source ST0 = source / ST0. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FDIV dest, ST0 dest /= ST0. La destination peut être n'importe quel registre du coprocesseur.

FDIVR dest, ST0 dest = ST0 / dest . La destination peut être n'importe quel registre du coprocesseur.

FDIVP dest ou FDIVP dest, ST0 dest /= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FDIVRP dest ou FDIVRP dest, ST0 dest = ST0 / dest puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIDIV source ST0 /= (float) source. Divise ST0 par un entier. La src doit être un mot ou un double-mot en mémoire.

FIDIVR source ST0 = (float) source / ST0. Divise un entier par ST0. La source doit être un mot ou un double-mot en mémoire.

VII-C-2-d. Comparaisons

Le coprocesseur effectue également des comparaisons de nombres en virgule flottante. La famille d'instructions FCOM est faite pour ça.

FCOM source compare ST0 et source. La source peut être un registre du coprocesseur ou un float ou un double en mémoire.

FCOMP source compare ST0 et source, puis retire ST0 de la pile. La source peut être un registre du coprocesseur ou un float ou un double en mémoire.

FCOMPP compare ST0 et ST1, puis retire ST0 et ST1 de la pile.

FICOM source compare ST0 et (float) source. La source peut être un entier sur un mot ou un double-mot en mémoire.

FICOMP source compare ST0 et (float)source, puis retire ST0 de la pile. La source peut être un entier sur un mot ou un double-mot en mémoire.

FTST compare ST0 et 0,

Ces instructions changent les bits C0, C1, C2 et C3 du registre de statut du coprocesseur. Malheureusement, il n'est pas possible pour le processeur d'accéder à ces bits directement. Les instructions de branchement conditionnel utilisent le registre FLAGS, pas le registre de statut du coprocesseur.

Cependant, il est relativement simple de transférer les bits du mot de statut dans les bits correspondants du registre FLAGS en utilisant quelques instructions nouvelles :

FSTSW destination stocke le mot de statut du coprocesseur soit dans un mot en mémoire soit dans le registre AX.

SAHF stocke le registre AH dans le registre FLAGS.

LAHF charge le registre AH avec les bits du registre FLAGS.

La Figure 6.6 montre un court extrait de code en exemple.

Image non disponible

Les lignes 5 et 6 transfèrent les bits C0, C1, C2 et C3 du mot de statut du coprocesseur dans le registre FLAGS. Les bits sont transférés de façon à être identiques au résultat d'une comparaison de deux entiers non signés. C'est pourquoi la ligne 7 utilise une instruction JNA.

Les Pentium Pro (et les processeurs plus récents (Pentium II and III)) supportent deux nouveaux opérateurs de comparaison qui modifient directement le registre FLAGS du processeur.

FCOMI source compare ST0 et source. La source doit être un registre du coprocesseur.

FCOMIP source compare ST0 et source, puis retire ST0 de la pile. La source doit être un registre du coprocesseur.

La Figure 6.7 montre une sous-routine qui trouve le plus grand de deux double en utilisant l'instruction FCOMIP. Ne confondez pas ces instructions avec les fonctions de comparaisons avec un entier (FICOM et FICOMP).

Image non disponible

VII-C-2-e. Instructions diverses

Cette section traite de diverses autres instructions fournies par le coprocesseur.

FCHS ST0 = - ST0 change le signe de ST0.

FABS ST0 = |ST0| extrait la valeur absolue de ST0.

FSQRT extrait la racine carrée de ST0.

FSCALE ST0 = ST0 × 2[ST1] multiplie ST0 par une puissance de 2 rapidement. ST1 n'est pas retiré de la pile du coprocesseur.

La Figure 6.8 montre un exemple d'utilisation de cette instruction.

Image non disponible

VII-C-3. Exemple : formule quadratique

Le premier exemple montre comment la formule quadratique peut être codée en assembleur. Souvenez-vous, la formule quadratique calcule les solutions de l'équation :

ax2 + bx + c = 0

La formule donne deux solutions pour x : x1 et x2.

Image non disponible

L'expression sous la racine carrée (b2 − 4ac) est appelée le déterminant. Sa valeur est utile pour déterminer laquelle des trois possibilités suivantes est vérifiée pour les solutions :

1. Il n'y a qu'une seule solution double. b2 − 4ac = 0 ;

2. Il y a deux solutions réelles. b2 − 4ac > 0 ;

3. Il y a deux solutions complexes. b2 − 4ac < 0.

Voici un court programme C qui utilise la sous-routine assembleur :

quadt.c
Sélectionnez
2
3 int quadratic ( double, double, double, double , double );
4
5  int main()
6 {
7 double a,b,c , root1 , root2;
8
9 printf ("Entrez a , b, c : ");
10 scanf("%lf %lf %lf", &a, &b, &c);
11 if ( quadratic ( a , b, c, &root1, &root2 ) )
12 printf (" racines : %.10g %.10g\n", root1, root2 );
13 else
14 printf ("pas de racine réelle \n");
15 return 0;
16 }

Voici la routine assembleur :

quad.asm
Sélectionnez
1 ; fonction quadratic
2 ; trouve les solutions à l'équation quadratique :
3 ; a*x**2 + b*x + c = 0
4 ; prototype C :
5 ; int quadratic( double a, double b, double c,
6 ; double * root1, double *root2 )
7 ; Paramètres :
8 ; a, b, c - coefficients des puissances de l'équation quadratique (voir ci-dessus)
9 ; root1 - pointeur vers un double où stocker la première racine
10 ; root2 - pointeur vers un double où stocker la deuxième racine
11 ; Valeur de retour :
12 ; retourne 1 si des racines réelles sont trouvées, sinon 0
13
14 ?fine a qword [ebp+8]
15 ?fine b qword [ebp+16]
16 ?fine c qword [ebp+24]
17 ?fine root1 dword [ebp+32]
18 ?fine root2 dword [ebp+36]
19 ?fine disc qword [ebp-8]
20 ?fine one_over_2a qword [ebp-16]
21
22 segment .data
23 MinusFour dw -4
24
25 segment .text
26 global _quadratic
27 _quadratic:
28 push ebp
29 mov ebp, esp
30 sub esp, 16 ; alloue 2 doubles (disc & one_over_2a)
31 push ebx ; on doit sauvegarder l'ebx original
32
33 fild word [MinusFour]; pile : -4
34 fld a ; pile : a, -4
35 fld c ; pile : c, a, -4
36 fmulp st1 ; pile : a*c, -4
37 fmulp st1 ; pile : -4*a*c
38 fld b
39 fld b ; pile : b, b, -4*a*c
40 fmulp st1 ; pile : b*b, -4*a*c
41 faddp st1 ; pile : b*b - 4*a*c
42 ftst ; compare avec 0
43 fstsw ax
44 sahf
45 jb no_real_solutions ; si < 0, pas de solution réelle
46 fsqrt ; pile : sqrt(b*b - 4*a*c)
47 fstp disc ; stocke et décale la pile
48 fld1 ; pile : 1,0
49 fld a ; pile : a, 1,0
50 fscale ; pile : a * 2(1,0) = 2*a, 1
51 fdivp st1 ; pile : 1/(2*a)
52 fst one_over_2a ; pile : 1/(2*a)
53 fld b ; pile : b, 1/(2*a)
54 fld disc ; pile : disc, b, 1/(2*a)
55 fsubrp st1 ; pile : disc - b, 1/(2*a)
56 fmulp st1 ; pile : (-b + disc)/(2*a)
57 mov ebx, root1
58 fstp qword [ebx] ; stocke dans *root1
59 fld b ; pile : b
60 fld disc ; pile : disc, b
61 fchs ; pile : -disc, b
62 fsubrp st1 ; pile : -disc - b
63 fmul one_over_2a ; pile : (-b - disc)/(2*a)
64 mov ebx, root2
65 fstp qword [ebx] ; stocke dans *root2
66 mov eax, 1 ; la valeur de retour est 1
67 jmp short quit
68
69 no_real_solutions :
70 mov eax, 0 ; la valeur de retour est 0
71
72 quit :
73 pop ebx
74 mov esp, ebp
75 pop ebp
76 ret

VII-C-4. Exemple : lire un tableau depuis un fichier

Dans cet exemple, une routine assembleur lit des doubles depuis un fichier. Voici un court programme de test en C :

readt.c
Sélectionnez
1 /
2 Ce programme teste la procédure assembleur 32 bits read_doubles().
3 Il lit des doubles depuis stdin (utilisez une redirection pour lire depuis un fichier).
4 /
5 #include <stdio.h>
6 extern int read_doubles( FILE , double , int );
7 #dene MAX 100
8
9 int main()
10 {
11 int i ,n;
12 double a[MAX];
13
14 n = read_doubles(stdin, a , MAX);
15
16 for ( i=0; i < n; i++ )
17 printf ("= %g\n", i, a[i ]);
18 return 0;
19 }

Voici la routine assembleur :

read.asm
Sélectionnez
1 segment .data
2 format db "%lf", 0 ; format pour fscanf()
3,
4 segment .text
5 global _read_doubles
6 extern _fscanf
7
8 ?fine SIZEOF_DOUBLE 8
9 ?fine FP dword [ebp + 8]
10 ?fine ARRAYP dword [ebp + 12]
11 ?fine ARRAY_SIZE dword [ebp + 16]
12 ?fine TEMP_DOUBLE [ebp - 8]
13
14 ;
15 ; fonction _read_doubles
16 ; prototype C :
17 ; int read_doubles( FILE * fp, double * arrayp, int array_size );
18 ; Cette fonction lit des doubles depuis un fichier texte dans un tableau, jusqu'à
19 ; EOF ou que le tableau soit plein.
20 ; Paramètres :
21 ; fp - FILE pointeur à partir duquel lire (doit être ouvert en lecture)
22 ; arrayp - pointeur vers le tableau de double vers lequel lire
23 ; array_size - nombre d'éléments du tableau
24 ; Valeur de retour :
25 ; nombre de doubles stockés dans le tableau (dans EAX)
26
27 _read_doubles:
28 push ebp
29 mov ebp,esp
30 sub esp, SIZEOF_DOUBLE ; définit un double sur la pile
31
32 push esi ; sauve esi
33 mov esi, ARRAYP ; esi = ARRAYP
34 xor edx, edx ; edx = indice du tableau (initialement 0)
35
36 while_loop:
37 cmp edx, ARRAY_SIZE ; edx < ARRAY_SIZE ?
38 jnl short quit ; si non, quitte la boucle
39 ;
40 ; appelle fscanf() pour lire un double dans TEMP_DOUBLE
41 ; fscanf() peut changer edx, donc on le sauvegarde
42 ;
43 push edx ; sauve edx
44 lea eax, TEMP_DOUBLE
45 push eax ; empile &TEMP_DOUBLE
46 push dword format ; emplie &format
47 push FP ; emplie file pointer
48 call _fscanf
49 add esp, 12
50 pop edx ; restaure edx
51 cmp eax, 1 ; fscanf a retourné 1 ?
52 jne short quit ; si non, on quitte la boucle
53
54 ;
55 ; copie TEMP_DOUBLE dans ARRAYP[edx]
56 ; (Les 8 octets du double sont copiés en deux fois 4 octets)
57 ;
58 mov eax, [ebp - 8]
59 mov [esi + 8*edx], eax ; copie des 4 octets de poids faible
60 mov eax, [ebp - 4]
61 mov [esi + 8*edx + 4], eax ; copie des 4 octets de poids fort
62
63 inc edx
64 jmp while_loop
65
66 quit :
67 pop esi ; restaure esi
68
69 mov eax, edx ; stocke la valeur de retour dans eax
70
71 mov esp, ebp
72 pop ebp
73 ret

VII-C-5. Exemple : rechercher les nombres premiers

Ce dernier exemple recherche les nombres premiers, une fois de plus.

Cette implémentation est plus efficace que la précédente. Elle stocke les nombres premiers déjà trouvés dans un tableau et ne divise que par ceux-ci au lieu de diviser par tous les nombres impairs pour trouver de nouveaux premiers.

Une autre différence est qu'il calcule la racine carrée du candidat pour le prochain premier afin de déterminer le moment où il peut s'arrêter de rechercher des facteurs. Il altère le fonctionnement du coprocesseur afin que lorsqu'il stocke la racine sous forme d'entier, il la tronque au lieu de l'arrondir.

Ce sont les bits 10 et 11 du mot de contrôle qui permettent de paramétrer cela. Ces bits sont appelés les bits RC (Rounding Control, contrôle d'arrondi).

S'ils sont tous les deux à 0 (par défaut), le coprocesseur arrondit lorsqu'il convertit vers un entier. S'ils sont tous les deux à 1, le copresseur tronque les conversions en entiers. Notez que la routine fait attention à sauvegarder le mot de contrôle original et à le restaurer avant de quitter.

Voici le programme C pilote :

fprime.c
Sélectionnez
1 #include <stdio.h>
2 #include <stdlib.h>
3 /
4 fonction nd_primes
5 recherche le nombre indiqué de nombres premiers
6 èParamtres :
7 a &#8722; tableau pour contenir les nombres premiers
8 n &#8722; nombre de nombres premiers à trouver
9 /
10 extern void nd_primes( int a , unsigned n );
11
12 int main()
13 {
14 int statut ;
15 unsigned i;
16 unsigned max;
17 int a;
18
19 printf ("Combien de nombres premiers voulez-vous trouver ? ");
20 scanf("%u", &max);
21
22 a = calloc ( sizeof(int ), max);
23
24 if ( a ) {
25
26 nd_primes(a,max);
27
28 / affiche les 20 derniers nombres premiers trouvés /
29 for( i= ( max > 20 ) ? max &#8722; 20 : 0; i < max; i++ )
30 printf ("= %d\n", i+1, a[i]);
31
32 free (a);
33 statut = 0;
34 }
35 else {
36 fprintf ( stderr , "Impossible de  créer un tableau de %u entiers\n", max);
37 statut = 1;
38 }
39
40 return statut ;
41 }

Voici la routine assembleur :

prime2.asm
Sélectionnez
1 segment .text
2 global _find_primes
3 ;
4 ; fonction find_primes
5 ; trouve le nombre indiqué de nombres premiers
6 ; Paramètres :
7 ; array - tableau pour contenir les nombres premiers
8 ; n_find - nombre de nombres premiers à trouver
9 ; Prototype C :
10 ;extern void find_primes( int * array, unsigned n_find )
11 ;
12 ?fine array ebp + 8
13 ?fine n_find ebp + 12
14 ?fine n ebp - 4 ; Nombre de nombres premiers trouvés
15 ?fine isqrt ebp - 8 ; racine du candidat
16 ?fine orig_cntl_wd ebp - 10 ; mot de contrôle original
17 ?fine new_cntl_wd ebp - 12 ; nouveau mot de contrôle
18
19 _find_primes :
20 enter 12,0 ; fait de la place pour les variables locales
21
22 push ebx ; sauvegarde les variables registre éventuelles
23 push esi
24
25 fstcw word [orig_cntl_wd] ; récupère le mot de contrôle courant
26 mov ax, [orig_cntl_wd]
27 or ax, 0C00h ; positionne les bits d'arrondi à 11 (tronquer)
28 mov [new_cntl_wd], ax
29 fldcw word [new_cntl_wd]
30
31 mov esi, [array] ; esi pointe sur array
32 mov dword [esi], 2 ; array[0] = 2
33 mov dword [esi + 4], 3 ; array[1] = 3
34 mov ebx, 5 ; ebx = guess = 5
35 mov dword [n], 2 ; n = 2
36 ;
37 ; Cette boucle externe trouve un nouveau nombre premier à chaque itération qu'il
38 ; ajoute à la fin du tableau. Contrairement au programme de recherche de nombres
39 ; premier précédent, cette fonction ne détermine pas la primauté du nombre en
40 ; divisant par tous les nombres impairs. Elle ne divise que par les nombres
41 ; premiers qu'elle a déjà trouvé (c'est pourquoi ils sont stockés dans un tableau).
42 ;
43 while_limit:
44 mov eax, [n]
45 cmp eax, [n_find] ; while ( n < n_find )
46 jnb short quit_limit
47
48 mov ecx, 1 ; ecx est utilisé comme indice
49 push ebx ; stocke le candidat sur la pile
50 fild dword [esp] ; le charge sur la pile du coprocesseur
51 pop ebx ; retire le candidat de la pile
52 fsqrt ; calcule sqrt(guess)
53 fistp dword [isqrt] ; isqrt = floor(sqrt(quess))
54 ;
55 ; Cette boucle interne divise le candidat (ebx) par les nombres premiers
56 ; calculés précédemment jusqu'à ce qu'il trouve un de ses facteurs premiers
57 ; (ce qui signifie que le candidat n'est pas premier) ou jusqu'à ce que le
58 ; nombre premier par lequel diviser soit plus grand que floor(sqrt(guess))
59 ;
60 while_factor:
61 mov eax, dword [esi + 4*ecx] ; eax = array[ecx]
62 cmp eax, [isqrt] ; while ( isqrt < array[ecx]
63 jnbe short quit_factor_prime
64 mov eax, ebx
65 xor edx, edx
66 div dword [esi + 4*ecx]
67 or edx, edx ; && guess % array[ecx] != 0 )
68 jz short quit_factor_not_prime
69 inc ecx ; essaie le nombre premier suivant
70 jmp short while_factor
71
72 ;
73 ; found a new prime !
74 ;
75 quit_factor_prime :
76 mov eax, [n]
77 mov dword [esi + 4*eax], ebx ; ajoute le candidat au tableau
78 inc eax
79 mov [n], eax ; incrémente n
80
81 quit_factor_not_prime :
82 add ebx, 2 ; essaie le nombre impair suivant
83 jmp short while_limit
84
85 quit_limit:
86
87 fldcw word [orig_cntl_wd] ; restaure le mot de contrôle
88 pop esi ; restaure les variables registre
89 pop ebx
90
91 leave
92 ret

VIII. Structures et C++

VIII-A. Structures

VIII-A-1. Introduction

Les structures sont utilisées en C pour regrouper des données ayant un rapport entre elles dans une variable composite. Cette technique a plusieurs avantages :

1. Cela clarifie le code en montrant que les données définies dans la structure sont intimement liées ;

2. Cela simplifie le passage des données aux fonctions. Au lieu de passer plusieurs variables séparément, elles peuvent être passées en une seule entité ;

3. Cela augmente la localité(32) du code.

Du point de vue de l'assembleur, une structure peut être considérée comme un tableau avec des éléments de tailles variables. Les éléments des vrais tableaux sont toujours de la même taille et du même type. C'est cette propriété qui permet de calculer l'adresse de n'importe quel élément en connaissant l'adresse de début du tableau, la taille des éléments et l'indice de l'élément voulu.

Les éléments d'une structure ne sont pas nécessairement de la même taille (et habituellement, ils ne le sont pas). À cause de cela, chaque élément d'une structure doit être explicitement spécifié et doit recevoir un tag (ou nom) au lieu d'un indice numérique.

En assembleur, on accède à un élément d'une structure d'une façon similaire à l'accès à un élément de tableau. Pour accéder à un élément, il faut connaître l'adresse de départ de la structure et le déplacement relatif de cet un tableau où ce déplacement peut être calculé grâce à l'indice de l'élément, c'est le compilateur qui affecte un déplacement aux éléments d'une structure.

Par exemple, considérons la structure suivante :

Sélectionnez
struct S {

short int x ; / entier sur 2 octets /

int y ; / entier sur 4 octets /

double z ; / ottant sur 8 octets /

};

La Figure 7.1 montre à quoi pourrait ressembler une variable de type S en mémoire.

Image non disponible

Le standard ANSI C indique que les éléments d'une structure sont organisés en mémoire dans le même ordre que celui de leur définition dans le struct. Il indique également que le premier élément est au tout début de la structure (i.e. au déplacement zéro). Il définit également une macro utile dans le fichier d'en-tête stddef.h appelée offsetof(). Cette macro calcule et renvoie le déplacement de n'importe quel élément d'une structure. La macro prend deux paramètres, le premier est le nom du type de la structure, le second est le nom de l'élément dont on veut le déplacement. Donc le résultat de offsetof(S, y) serait 2 d'après la Figure 7.1.

VIII-A-2. Alignement en mémoire

Si l'on utilise la macro offsetof pour trouver le déplacement de y en utilisant le compilateur gcc, on s'aperçoit qu'elle renvoie 4, pas 2 ! Pourquoi ?

Parce que gcc (et beaucoup d'autres compilateurs) aligne les variables sur des multiples de double-mot par défaut.

Souvenez-vous qu'une adresse est sur un multiple de double-mot si elle est divisible par 4.

En mode protégé 32 bits, le processeur lit la mémoire plus vite si la donnée commence sur un multiple de double-mot. La Figure 7.2 montre à quoi ressemble la structure S en utilisant gcc.

Image non disponible

Le compilateur insère deux octets inutilisés dans la structure pour aligner y (et z) sur un multiple de double-mot. Cela montre pourquoi c'est une bonne idée d'utiliser offsetof pour calculer les déplacements, au lieu de les calculer soi-même lorsqu'on utilise des structures en C.

Bien sûr, si la structure est utilisée uniquement en assembleur, le programmeur peut déterminer les déplacements lui-même. Cependant, si l'on interface du C et de l'assembleur, il est très important que l'assembleur et le C s'accordent sur les déplacements des éléments de la structure ! Une des complications est que des compilateurs C différents peuvent donner des déplacements différents aux éléments. Par exemple, comme nous l'avons vu, le compilateur gcc crée une structure S qui ressemble à la Figure 7.2, cependant, le compilateur de Borland créerait une structure qui ressemble à la Figure 7.1. Les compilateurs C fournissent le moyen de spécifier l'alignement utilisé pour les données. Cependant, le standard ANSI C ne spécifie pas comment cela doit être fait et donc, des compilateurs différents procèdent différemment.

Le compilateur gcc a une méthode flexible et compliquée de spécifier l'alignement. Le compilateur permet de spécifier l'alignement de n'importe quel type en utilisant une syntaxe spéciale. Par exemple, la ligne suivante :

Sélectionnez
typedef short int unaligned_int __attribute__((aligned(1)));

définit un nouveau type appelé unaligned_int qui est aligné sur des multiples d'octet (oui, toutes les parenthèses suivant __attribute__ sont nécessaires !) Le paramètre 1 de aligned peut être remplacé par d'autres puissances de 2 pour spécifier d'autres alignements (2 pour s'aligner sur les mots, 4 sur les doubles-mots, etc.). Si l'élément y de la structure était changé en un type unaligned_int, gcc placerait y au déplacement 2. Cependant, z serait toujours au déplacement 8 puisque les doubles sont également alignés sur des doubles-mots par défaut. La définition du type de z devrait aussi être changée pour le placer au déplacement 6.

Le compilateur gcc permet également de comprimer (pack) une structure.

Cela indique au compilateur d'utiliser le minimum d'espace possible pour la structure. La Figure 7.3 montre comment S pourrait être réécrite de cette façon. Cette forme de S utiliserait le moins d'octets possible, soit 14 octets.

Image non disponible

Les compilateurs de Microsoft et Borland supportent tous les deux la même méthode pour indiquer l'alignement par le biais d'une directive #pragma.

Sélectionnez
#pragma pack(1)

La directive ci-dessus indique au compilateur d'aligner les éléments des structures sur des multiples d'un octet (i.e., sans décalage superflu). Le 1 peut être remplacé par 2, 4, 8 ou 16 pour spécifier un alignement sur des multiples de mots, doubles-mots, quadruples-mots ou de paragraphe, respectivement. La directive reste active jusqu'à ce qu'elle soit écrasée par une autre. Cela peut poser des problèmes puisque ces directives sont souvent utilisées dans des fichiers d'en-tête. Si le fichier d'en-tête est inclus avant d'autres fichiers d'en-tête définissant des structures, ces structures peuvent être organisées différemment de ce qu'elles auraient été par défaut. Cela peut conduire à des erreurs très difficiles à localiser. Les différents modules d'un programme devraient organiser les éléments des structures à différents endroits !

Il y a une façon d'éviter ce problème. Microsoft et Borland permettent la sauvegarde de l'état de l'alignement courant et sa restauration. La Figure 7.4 montre comment on l'utilise.

Image non disponible

VIII-A-3. Champs de bits

Les champs de bits permettent de déclarer des membres d'une structure qui n'utilisent qu'un nombre de bits donné. La taille en bits n'a pas besoin d'être un multiple de 8. Un membre champ de bit est défini comme un unsigned int ou un int suivi de deux-points et de sa taille en bits. La Figure 7.5 en montre un exemple.

Image non disponible

Elle définit une variable 32 bits décomposée comme suit :

Image non disponible

Le premier champ de bits est assigné aux bits les moins significatifs du double-mot(33).

Néanmoins, le format n'est pas si simple si l'on observe comment les bits sont stockés en mémoire. La difficulté apparaît lorsque les champs de bits sont à cheval sur des multiples d'octets. Car les octets, sur un processeur little endian seront inversés en mémoire. Par exemple, les champs de bits de la structure S ressembleront à cela en mémoire :

Image non disponible

L'étiquette f2l fait référence aux cinq derniers bits (i.e., les cinq bits les moins significatifs) du champ de bits f2. L'étiquette f2m fait référence aux cinq bits les plus significatifs de f2. Les lignes verticales doubles montrent les limites d'octets. Si l'on inverse tous les octets, les morceaux des champs f2 et f3 seront réunis correctement.

L'organisation de la mémoire physique n'est habituellement pas importante à moins que des données soient transférées depuis ou vers le programme (ce qui est en fait assez courant avec les champs de bits). Il est courant que les interfaces de périphériques matériels utilisent des nombres impairs de bits dont les champs de bits facilitent la représentation.

Un bon exemple est SCSI(34). Une commande de lecture directe pour un périphérique SCSI est spécifiée en envoyant un message de six octets au périphérique selon le format indiqué dans la Figure 7.6.

Image non disponible

La difficulté de représentation en utilisant les champs de bits est l'adresse de bloc logique qui est à cheval sur trois octets différents de la commande. D'après la Figure 7.6, on constate que les données sont stockées au format big endian. La Figure 7.7 montre une définition qui essaie de fonctionner avec tous les compilateurs.

Image non disponible

Les deux premières lignes définissent une macro qui est vraie si le code est compilé avec un compilateur Borland ou Microsoft. La partie qui peut porter à confusion va des lignes 11 à 14. Tout d'abord, on peut se demander pourquoi les champs lba_mid et lba_lsb sont définis séparément et non pas comme un champ unique de 16 bits. C'est parce que les données sont stockées au format big endian. Un champ de 16 bits serait stocké au format little endian par le compilateur. Ensuite, les champs lba_msb et logical_unit semblent être inversés, cependant, ce n'est pas le cas. Ils doivent être placés dans cet ordre. La Figure 7.8 montre comment les champs sont organisés sous la forme d'une entité de 48 bits (les limites d'octets sont là encore représentées par des

lignes doubles).

Image non disponible

Lorsqu'elle est stockée en mémoire au format little endian, les bits sont réarrangés au format voulu (Figure 7.6).

Pour compliquer encore plus le problème, la définition de SCSI_read_cmd ne fonctionne pas correctement avec le C Microsoft. Si l'expression sizeof(SCSI_read_cmd) est évaluée, le C Microsoft renvoie 8 et non pas 6 ! C'est parce que le compilateur Microsoft utilise le type du champ de bits pour déterminer comment organiser les bits. Comme tous les bits sont déclarés comme unsigned, le compilateur ajoute deux octets à la fin de la structure pour qu'elle comporte un nombre entier de doubles-mots. Il est possible d'y remédier en déclarant tous les champs unsigned short. Maintenant, le compilateur Microsoft n'a plus besoin d'ajouter d'octets d'alignement puisque six octets forment un nombre entier de mots de deux octets(35). Les autres compilateurs fonctionnent également correctement avec ce changement. La Figure 7.9 montre une autre définition qui fonctionne sur les trois compilateurs.

Image non disponible

Elle ne déclare plus que deux champs de bits en utilisant le type unsigned char.

Le lecteur ne doit pas se décourager s'il trouve la discussion ci-dessus confuse. C'est confus ! L'auteur trouve souvent moins confus d'éviter d'utiliser des champs de bits en utilisant des opérations niveau bit pour examiner et modifier les bits manuellement.

VIII-A-4. Utiliser des structures en assembleur

Comme nous l'avons dit plus haut, accéder à une structure en assembleur ressemble beaucoup à accéder à un tableau. Prenons un exemple simple, regardons comment l'on pourrait écrire une routine assembleur qui mettrait à zéro l'élément y d'une structure S. Supposons que le prototype de la routine soit :

Sélectionnez
void zero_y( S s_p );

La routine assembleur serait :

Sélectionnez
1 ?fine y_offset 4
2 _zero_y :
3 enter 0,0
4 mov eax, [ebp + 8] ; récupère s_p depuis la pile
5 mov dword [eax + y_offset], 0
6 leave
7 ret

Le C permet de passer une structure par valeur à une fonction. Cependant, c'est une mauvaise idée la plupart du temps. Toutes les données de la structure doivent être copiées sur la pile puis récupérées par la routine. Il est beaucoup plus efficace de passer un pointeur vers la structure à la place.

Le C permet aussi qu'une fonction renvoie une structure. Évidemment, une structure ne peut pas être retournée dans le registre EAX. Des compilateurs différents gèrent cette situation de façon différente. Une situation courante que les compilateurs utilisent est de réécrire la fonction en interne de façon à ce qu'elle prenne un pointeur sur la structure en paramètre. Le pointeur est utilisé pour placer la valeur de retour dans une structure définie en dehors de la routine appelée.

La plupart des assembleurs (y compris NASM) ont un support intégré pour définir des structures dans votre code assembleur. Reportez-vous à votre documentation pour plus de détails.

VIII-B. Assembleur et C++

Le langage de programmation C++ est une extension du langage C. Beaucoup des règles valables pour interfacer le C et l'assembleur s'appliquent également au C++. Cependant, certaines règles doivent être modifiées. De plus, certaines extensions du C++ sont plus faciles à comprendre en connaissant le langage assembleur. Cette section suppose une connaissance basique du C++.

VIII-B-1. Surcharge et décoration de noms

Le C++ permet de définir des fonctions (et des fonctions membres) différentes avec le même nom. Lorsque plus d'une fonction partage le même nom, les fonctions sont dites surchargées. Si deux fonctions sont définies avec le même nom en C, l'éditeur de liens produira une erreur, car il trouvera deux définitions pour le même symbole dans les fichiers objet qu'il est en train de lier. Par exemple, prenons le code de la Figure 7.10.

Image non disponible

Le code assembleur équivalent définirait deux étiquettes appelées _f ce qui serait bien sûr une

erreur. Le C++ utilise le même procédé d'édition de liens que le C, mais évite cette erreur en effectuant une décoration de nom (name mangling) ou en modifiant le symbole utilisé pour nommer une fonction. D'une certaine façon, le C utilise déjà la décoration de nom. Il ajoute un caractère de soulignement au nom de la fonction C lorsqu'il crée l'étiquette pour la fonction. Cependant, il décorera le nom des deux fonctions de la Figure 7.10 de la même façon et produira une erreur. Le C++ utilise un procédé de décoration plus sophistiqué qui produit deux étiquettes différentes pour les fonctions. Par exemple, la première fonction de la Figure 7.10 recevrait l'étiquette _f__Fi et la seconde, _f__Fd, sous DJGPP. Cela évite toute erreur d'édition de liens.

Malheureusement, il n'y a pas de standard sur la gestion des noms en C++ et des compilateurs différents décorent les noms de façon différente.

Par exemple, Borland C++ utiliserait les étiquettes @f$qi et @f$qd pour les deux fonctions de la Figure 7.10. Cependant, les règles ne sont pas totalement arbitraires. Le nom décoré encode la signature de la fonction. La signature d'une fonction est donnée par l'ordre et le type de ses paramètres. Notez que la fonction qui ne prend qu'un argument int a un i à la fin de son nom décoré (à la fois sous DJGPP et Borland) et que celle qui prend un argument double a un d à la fin de son nom décoré. S'il y avait une fonction appelée f avec le prototype suivant :

Sélectionnez
void f ( int x , int y , double z);

DJGPP décorerait son nom en _f__Fiid et Borland en @f$qiid.

Le type de la fonction ne fait pas partie de la signature d'une fonction et n'est pas encodé dans le nom décoré. Ce fait explique une règle de la surcharge en C++. Seules les fonctions dont les signatures sont uniques peuvent être surchargées. Comme on le voit, si deux fonctions avec le même nom et la même signature sont définies en C++, elles donneront le même nom décoré et créeront une erreur lors de l'édition de liens. Par défaut, toutes les fonctions C++ sont décorées, même celles qui ne sont pas surchargées. Lorsqu'il compile un fichier, le compilateur n'a aucun moyen de savoir si une fonction particulière est surchargée ou non, il décore donc tous les noms. En fait, il décore également les noms des variables globales en encodant le type de la variable d'une façon similaire à celle utilisée pour les signatures de fonctions.

Donc, si l'on définit une variable globale dans un fichier avec un certain type puis que l'on essaie de l'utiliser dans un autre fichier avec le mauvais type, l'éditeur de liens produira une erreur. Cette caractéristique du C++ est connue sous le nom de typesafe linking (édition de liens avec respect des types) . Cela crée un autre type d'erreurs, les prototypes inconsistants. Cela arrive lorsque la définition d'une fonction dans un module ne correspond pas avec le prototype utilisé par un autre module. En C, cela peut être un problème très difficile à corriger. Le C ne détecte pas cette erreur. Le programme compilera et sera lié, mais aura un comportement imprévisible, car le code appelant placera sur la pile des types différents de ceux que la fonction attend. En C++ cela produira une erreur lors de l'édition de liens.

Lorsque le compilateur C++ analyse un appel de fonction, il recherche la fonction correspondante en observant les arguments qui lui sont passés(36).

S'il trouve une correspondance, il crée un CALL vers la fonction adéquate en utilisant les règles de décoration du compilateur.

Comme des compilateurs différents utilisent différentes règles de décoration de nom, il est possible que des codes C++ compilés par des compilateurs différents ne puissent pas être liés ensemble. C'est important lorsque l'on a l'intention d'utiliser une bibliothèque C++ précompilée ! Si l'on veut écrire une fonction en assembleur qui sera utilisée avec du code C++, il faut connaître les règles de décoration de nom du compilateur C++ utilisé (ou utiliser la technique expliquée plus bas).

L'étudiant astucieux pourrait se demander si le code de la Figure 7.10 fonctionnera de la façon attendue. Comme le C++ décore toutes les fonctions, alors la fonction printf sera décorée et le compilateur ne produira pas un CALL à l'étiquette _printf. C'est une question pertinente. Si le prototype de printf était simplement placé au début du fichier, cela arriverait. Son prototype est :

Sélectionnez
int printf ( const char , ...);

DJGPP décorerait ce nom en _printf__FPCce (F pour fonction, P pour pointeur, C pour const, c pour char et e pour ellipse). Cela n'appellerait pas la fonction printf de la bibliothèque C standard ! Bien sûr, il doit y avoir un moyen pour que le C++ puisse appeler du code C. C'est très important, car il existe une énorme quantité de vieux code C utile. En plus de permettre l'accès au code hérité de C, le C++ permet également d'appeler du code assembleur en utilisant les conventions de décoration standard du C.

Le C++ étend le mot-clé extern pour lui permettre de spécifier que la fonction ou la variable globale qu'il modifie utilise les conventions C normales.

Dans la terminologie C++, la fonction ou la variable globale utilise une édition de liens C. Par exemple, pour déclarer la fonction printf comme ayant une édition de liens C, utilisez le prototype :

Sélectionnez
extern "C" int printf ( const char , ... );

Cela impose au compilateur de ne pas utiliser les règles de décoration de nom du C++ sur la fonction, mais d'utiliser les règles C à la place. Cependant, en faisant cela, la fonction printf ne peut pas être surchargée. Cela constitue la façon la plus simple d'interfacer du C++ et de l'assembleur, définir une fonction comme utilisant une édition de liens C puis utiliser la convention d'appel C.

Pour plus de facilité, le C++ permet également de définir une édition de liens C sur un bloc de fonctions et de variables globales. Le bloc est indiqué en utilisant les accolades habituelles.

Sélectionnez
extern "C" {

/ variables globales et prototypes des fonctions ayant une édition de liens C /

}

Si l'on examine les fichiers d'en-tête ANSI C fournis avec les compilateurs C/C++ actuels, on trouve ce qui suit vers le début de chaque fichier d'entête :

Sélectionnez
#ifdef __cplusplus

extern "C" {

#endif

Et une construction similaire, vers la fin, contenant une accolade fermante.

Les compilateurs C++ définissent la macro __cplusplus (avec deux caractères de soulignement au début). L'extrait ci-dessus entoure tout le fichier d'en-tête dans un bloc extern "C" si le fichier d'en-tête est compilé en C++, mais ne fait rien s'il est compilé en C (puisqu'un compilateur C génèrerait une erreur de syntaxe sur extern "C"). La même technique peut être utilisée par n'importe quel programmeur pour créer un fichier d'en-tête pour des routines assembleur pouvant être utilisées en C ou en C++.

VIII-B-2. Références

Les références sont une autre nouvelle fonctionnalité du C++. Elles permettent de passer des paramètres à une fonction sans utiliser explicitement de pointeur. Par exemple, considérons le code de la Figure 7.11.

Image non disponible

En fait, les paramètres par référence sont plutôt simples, ce sont des pointeurs. Le compilateur masque simplement ce fait aux yeux du programmeur (exactement de la même façon que les compilateurs Pascal qui implémentent les paramètres var comme des pointeurs). Lorsque le compilateur génère l'assembleur pour l'appel de fonction ligne 7, il passe l'adresse de y. Si l'on écrivait la fonction f en assembleur, on ferait comme si le prototype était(37) :

Sélectionnez
void f ( int xp);

Les références sont juste une facilité qui est particulièrement utile pour la surcharge d'opérateurs. C'est une autre fonctionnalité du C++ qui permet de donner une signification aux opérateurs de base lorsqu'ils sont appliqués à des structures ou des classes. Par exemple, une utilisation courante est de définir l'opérateur plus (+) de façon à ce qu'il concatène les objets string.

Donc, si a et b sont des strings, a + b renverra la concaténation des chaînes a et b. Le C++ appellerait en réalité une fonction pour ce faire (en fait, cette expression pourrait être réécrite avec une notation fonction sous la forme operator +(a,b)). Pour plus d'efficacité, il est souhaitable de passer l'adresse des objets string à la place de les passer par valeur. Sans référence, cela pourrait être fait en écrivant operator +(&a,&b), mais cela imposerait d'écrire l'opérateur avec la syntaxe &a + &b. Cela serait très maladroit et confus. Par contre, en utilisant les références, il est possible d'écrire a + b, ce qui semble très naturel.

VIII-B-3. Fonctions inline

Les fonctions inline sont encore une autre fonctionnalité du C++(38). Les fonctions inline sont destinées à remplacer les macros du préprocesseur qui prennent des paramètres, sources d'erreur. Sourvenez-vous en C, une macro qui élève un nombre au carré ressemble à cela :

Sélectionnez
#dene SQR(x) ((x)(x))

Comme le préprocesseur ne comprend pas le C et ne fait que de simples substitutions, les parenthèses sont requises pour calculer le résultat correct dans la plupart des cas. Cependant, même cette version ne donnerait pas la bonne réponse pour SQR(x++).

Les macros sont utilisées, car elles éliminent la surcharge d'un appel pour une fonction simple. Comme le chapitre sur les sous-programmes l'a démontré, effectuer un appel de fonction implique plusieurs étapes. Pour une fonction très simple, le temps passé à l'appeler peut être plus grand que celui passé dans la fonction ! Les fonctions inline sont une façon beaucoup plus pratique d'écrire du code qui ressemble à une fonction, mais qui n'effectue pas de CALL à un bloc commun. Au lieu de cela, les appels à des fonctions inline sont remplacés par le code de la fonction. Le C++ permet de rendre une fonction inline en plaçant le mot-clé inline au début de sa définition.

Par exemple, considérons les fonctions déclarées dans la Figure 7.12.

Image non disponible

L'appel à la fonction f, ligne 10, est un appel de fonction normal (en assembleur, en supposant que x est à l'adresse ebp-8 et y en ebp-4) :

Sélectionnez
1 push dword [ebp-8]

2 call _f

3 pop ecx

4 mov [ebp-4], eax

Cependant, l'appel à la fonction inline_f, ligne 11 ressemblerait à :

Sélectionnez
1 mov eax, [ebp-8]

2 imul eax, eax

3 mov [ebp-4], eax

Dans ce cas, il y a deux avantages à inliner. Tout d'abord, la fonction inline est plus rapide. Aucun paramètre n'est placé sur la pile, aucun cadre de pile n'est créé puis détruit, aucun branchement n'est effectué. Ensuite, l'appel à la fonction inline utilise moins de code ! Ce dernier point est vrai pour cet exemple, mais ne reste pas vrai dans tous les cas.

La principal inconvénient de l'inlining est que le code inline n'est pas lié et donc le code d'une fonction inline doit être disponible pour tous les fichiers qui l'utilisent. L'exemple de code assembleur précédent le montre. L'appel de la fonction non-inline ne nécessite que la connaissance des paramètres, du type de valeur de retour, de la convention d'appel et du nom de l'étiquette de la fonction. Toutes ces informations sont disponibles par le biais du prototype de la fonction. Cependant, l'utilisation de la fonction inline nécessite la connaissance de tout le code de la fonction. Cela signifie que si n'importe quelle partie de la fonction change, tous les fichiers source qui utilisent la fonction doivent être recompilés. Souvenez-vous que pour les fonctions non inline, si le prototype ne change pas, souvent les fichiers qui utilisent la fonction n'ont pas besoin d'être recompilés. Pour toutes ces raisons, le code des fonctions inline est généralement placé dans les fichiers d'en-tête. Cette pratique est contraire à la règle stricte habituelle du C selon laquelle on ne doit jamais placer de code exécutable dans les fichiers d'en-tête.

VIII-B-4. Classes

Une classe C++ décrit un type d'objet. Un objet possède à la fois des membres données et des membres fonctions(39). En d'autres termes, il s'agit d'une struct à laquelle sont associées des données et des fonctions. Considérons la classe simple dénie dans la Figure 7.13.

Image non disponible

Une variable de type Simple ressemblerait à une struct C normale avec un seul membre int.

Les fonctions ne sont pas affectées à la structure en mémoire. Cependant, les fonctions membres sont différentes des autres fonctions. On leur passe un paramètre caché. Ce paramètre est un pointeur vers l'objet sur lequel agit la fonction.

En fait, le C++ utilise le mot-clé this pour accéder au pointeur vers l'objet lorsque l'on se trouve à l'intérieur d'une fonction membre.

Par exemple, considérons la méthode set_data de la classe Simple de la Figure 7.13. Si elle était écrite en C, elle ressemblerait à une fonction à laquelle on passerait explicitement un pointeur vers l'objet sur lequel elle agit comme le montre le code de la Figure 7.14.

Image non disponible

L'option -S du compilateur DJGPP (et des compilateurs gcc et Borland également) indique au compilateur de produire un fichier assembleur contenant l'équivalent assembleur du code. Pour DJGPP et gcc le fichier assembleur possède une extension .s et utilise malheureusement la syntaxe du langage assembleur AT&T qui est assez différente des syntaxes NASM et MASM(40) (les compilateurs Borland et MS génèrent un fichier avec l'extension .asm utilisant la syntaxe MASM). La Figure 7.15 montre la sortie de DJGPP convertie en syntaxe NASM avec des commentaires supplémentaires pour clarifier le but des instructions.

Image non disponible

Sur la toute première ligne, notez que la méthode set_data reçoit une étiquette décorée qui encode le nom de la méthode, le nom de la classe et les paramètres.

Le nom de la classe est encodé, car d'autres classes peuvent avoir une méthode appelée set_data et les deux méthodes doivent recevoir des étiquettes différentes. Les paramètres sont encodés afin que la classe puisse surcharger la méthode set_data afin qu'elle prenne d'autres paramètres, comme les fonctions C++ normales. Cependant, comme précédemment, des compilateurs différents encoderont ces informations différemment dans l'étiquette.

Ensuite, aux lignes 2 et 3 nous retrouvons le prologue habituel. À la ligne 5, le premier paramètre sur la pile est stocké dans EAX. Ce n'est pas le paramètre x ! Il s'agit du paramètre caché(41) qui pointe vers l'objet sur lequel on agit.

La ligne 6 stocke le paramètre x dans EDX et la ligne 7 stocke EDX dans le double-mot sur lequel pointe EAX. Il s'agit du membre data de l'objet Simple sur lequel on agit, qui, étant la seule donnée de la classe, est stocké au déplacement 0 de la structure Simple.

VIII-B-4-a. Exemple

Cette section utilise les idées de ce chapitre pour créer une classe C++ qui représente un entier non signé d'une taille arbitraire. Comme l'entier peut faire n'importe quelle taille, il sera stocké dans un tableau d'entiers non signés (doubles-mots). Il peut faire n'importe quelle taille en utilisant l'allocation dynamique. Les doubles-mots sont stockés dans l'ordre inverse(42) (i.e. le double-mot le moins significatif est au déplacement 0). La Figure 7.16 montre la définition de la classe Big_int(43).

Image non disponible
Fig. 7.16 Dénition de la classe Big_int

La taille d'un Big_int est mesurée par la taille du tableau d'unsigned utilisé pour stocker les données.

La donnée membre size_ de la classe est affectée au déplacement 0 et le membre number_ est affecté au déplacement 4.

Pour simplifier l'exemple, seuls les objets ayant des tableaux de la même taille peuvent être additionnés entre eux.

La classe a trois constructeurs : le premier (ligne 9) initialise l'instance de la classe en utilisant un entier non signé normal, le second (ligne 19) initalise l'instance en utilisant une chaîne qui contient une valeur hexadécimale. Le troisième constructeur (ligne 22) est le constructeur par copie.

Cette explication se concentre sur la façon dont fonctionnent les opérateurs d'addition et de soustraction, car c'est là que l'on utilise de l'assembleur.

La Figure 7.17 montre les parties du fichier d'en-tête relatives à ces opérateurs.

Image non disponible

Elles montrent comment les opérateurs sont paramétrés pour appeler des routines assembleur. Comme des compilateurs différents utilisent des règles de décoration radicalement différentes pour les fonctions opérateur, des fonctions opérateur inline sont utilisées pour initialiser les appels aux routines assembleur liées au format C. Cela les rend relativement simples à porter sur des compilateurs différents et est aussi rapide qu'un appel direct.

Cette technique élimine également le besoin de soulever une exception depuis l'assembleur !

Pourquoi l'assembleur n'est-il utilisé qu'ici ? Souvenez-vous que pour effectuer de l'arithmétique en précision multiple, la retenue doit être ajoutée au double mot significatif suivant. Le C++ (et le C) ne permet pas au programmeur d'accéder au drapeau de retenue du processeur. On ne pourrait effectuer l'addition qu'en recalculant indépendamment en C++ la valeur du drapeau de retenue et en l'ajoutant de façon conditionnelle au double-mot suivant. Il est beaucoup plus efficace d'écrire le code en assembleur à partir duquel on peut accéder au drapeau de retenue et utiliser l'instruction ADC qui ajoute automatiquement le drapeau de retenue.

Par concision, seule la routine assembleur add_big_ints sera expliquée ici. Voici le code de cette routine (contenu dans big_math.asm) :

big_math.asm
Sélectionnez
1 segment .text
2 global add_big_ints, sub_big_ints
3 ?fine size_offset 0
4 ?fine number_offset 4
5
6 ?fine EXIT_OK 0
7 ?fine EXIT_OVERFLOW 1
8 ?fine EXIT_SIZE_MISMATCH 2
9
10 ; Paramètres des routines add et sub
11 ?fine res ebp+8
12 ?fine op1 ebp+12
13 ?fine op2 ebp+16
14
15 add_big_ints:
16 push ebp
17 mov ebp, esp
18 push ebx
19 push esi
20 push edi
21 ;
22 ; initialise esi pour pointer vers op1
23 ; edi pour pointer vers op2
24 ; ebx pour pointer vers res
25 mov esi, [op1]
26 mov edi, [op2]
27 mov ebx, [res]
28 ;
29 ; s'assure que les 3 Big_int ont la même taille
30 ;
31 mov eax, [esi + size_offset]
32 cmp eax, [edi + size_offset]
33 jne sizes_not_equal ; op1.size_ != op2.size_
34 cmp eax, [ebx + size_offset]
35 jne sizes_not_equal ; op1.size_ != res.size_
36
37 mov ecx, eax ; ecx = taille des Big_int
38 ;
39 ; initialise les registres pour qu'ils pointent vers leurs tableaux respectifs
40 ; esi = op1.number_
41 ; edi = op2.number_
42 ; ebx = res.number_
43 ;
44 mov ebx, [ebx + number_offset]
45 mov esi, [esi + number_offset]
46 mov edi, [edi + number_offset]
47
48 clc ; met le drapeau de retenue à 0
49 xor edx, edx ; edx = 0
50 ;
51 ; boucle d'addition
52 add_loop:
53 mov eax, [edi+4*edx]
54 adc eax, [esi+4*edx]
55 mov [ebx + 4*edx], eax
56 inc edx ; ne modifie pas le drapeau de retenue
57 loop add_loop
58
59 jc overflow
60 ok_done:
61 xor eax, eax ; valeur de retour = EXIT_OK
62 jmp done
63 overflow:
64 mov eax, EXIT_OVERFLOW
65 jmp done
66 sizes_not_equal:
67 mov eax, EXIT_SIZE_MISMATCH
68 done:
69 pop edi
70 pop esi
71 pop ebx
72 leave
73 ret

Heureusement, la majorité de ce code devrait être compréhensible pour le lecteur maintenant. Les lignes 25 à 27 stockent les pointeurs vers les objets Big_int passés à la fonction via des registres. Souvenez-vous que les références sont des pointeurs. Les lignes 31 à 35 vérifient que les tailles des trois objets sont les mêmes (notez que le déplacement de size_ est ajouté au pointeur pour accéder à la donnée membre). Les lignes 44 à 46 ajustent les registres pour qu'ils pointent vers les tableaux utilisés par leurs objets respectifs au lieu des objets eux-mêmes (là encore, le déplacement du membre number_ est ajouté au pointeur sur l'objet).

La boucle des lignes 52 à 57 additionne les entiers stockés dans les tableaux en additionnant le double-mot le moins significatif en premier, puis les doubles-mots suivants, etc. L'addition doit être effectuée dans cet ordre pour l'arithmétique en précision étendue (voir Section III.A.5).

La ligne 59 vérifie qu'il n'y a pas de dépassement de capacité, lors d'un dépassement de capacité, le drapeau de retenue sera allumé par la dernière addition du double-mot le plus significatif. Comme les doubles-mots du tableau sont stockés dans l'ordre little endian, la boucle commence au début du tableau et avance jusqu'à la fin.

La Figure 7.18 montre un court exemple utilisant la classe Big_int.

Image non disponible

Notez que les constantes de Big_int doivent être déclarées explicitement comme à la ligne 16. C'est nécessaire pour deux raisons. Tout d'abord, il n'y a pas de constructeur de conversion qui convertisse un entier non signé en Big_int. Ensuite, seuls les Big_int de même taille peuvent être additionnés. Cela rend la conversion problématique puisqu'il serait difficile de savoir vers quelle taille convertir. Une implémentation plus sophistiquée de la classe permettrait d'additionner n'importe quelle taille avec n'importe quelle autre. L'auteur ne voulait pas compliquer inutilement cet exemple en l'implémentant ici (cependant, le lecteur est encouragé à le faire).

VIII-B-5. Héritage et polymorphisme

L'héritage permet à une classe d'hériter des données et des méthodes d'une autre. Par exemple, considérons le code de la Figure 7.19.

Image non disponible

Il montre deux classes, A et B, où la classe B hérite de A. La sortie du programme est :

Sélectionnez
Taille de a : 4 Déplacement de ad : 0

Taille de b : 8 Déplacement de ad: 0 Déplacement de bd: 4

A::m()

A::m()

Notez que les membres ad des deux classes (B l'hérite de A) sont au même déplacement. C'est important puisque l'on peut passer à la fonction f soit un pointeur vers un objet A soit un pointeur vers un objet de n'importe quel type dérivé de (i.e. qui hérite de) A. La Figure 7.20 montre le code assembleur (édité) de la fonction (généré par gcc).

Image non disponible

Notez que la sortie de la méthode m de A a été produite à la fois par l'objet a et l'objet b. D'après l'assembleur, on peut voir que l'appel à A::m() est codé en dur dans la fonction. Dans le cadre d'une vraie programmation orientée objet, la méthode appelée devrait dépendre du type d'objet passé à la fonction. On appelle cela le polymorphisme. Le C++ désactive cette fonctionnalité par défaut. On utilise le mot-clé virtual pour l'activer. La Figure 7.21 montre comment les deux classes seraient modifiées.

Image non disponible

Rien dans le restet du code n'a besoin d'être changé. Le polymorphisme peut être implémenté de beaucoup de manières. Malheureusement, l'implémentation de gcc est en transition au moment d'écrire ces lignes et devient beaucoup plus compliquée que l'implémentation initiale. Afin de simplifier cette explication, l'auteur ne couvrira que l'implémentation du polymorphisme que les compilateurs Microsoft et Borland utilisent. Cette implémentation n'a pas changé depuis des années et ne changera probablement pas dans un futur proche.

Avec ces changements, la sortie du programme change :

Sélectionnez
Size of a: 8 Déplacement de ad : 4

Size of b: 12 Déplacement de ad : 4 Déplacement de bd : 8

A::m()

B::m()

Maintenant, le second appel à f appelle la méthode B::m(), car on lui passe un objet B. Ce n'est pas le seul changement cependant. La taille d'un A vaut maintenant 8 (et 12 pour B). De plus, le déplacement de ad vaut maintenant 4, plus 0. Qu'y a-t-il au déplacement 0 ? La réponse à cette question est liée à la façon dont est implémenté le polymorphisme.

Une classe C++ qui a une (ou plusieurs) méthode(s) virtuelle(s) a un champ caché qui est un pointeur vers un tableau de pointeurs sur des méthodes(44). Cette table est souvent appelée la vtable. Pour les classes A et B ce pointeur est stocké au déplacement 0. Les compilateurs Windows placent toujours ce pointeur au début de la classe au sommet de l'arbre d'héritage.

En regardant le code assembleur (Figure 7.22) généré pour la fonction f (de la Figure 7.19) dans la version du programme avec les méthodes virtuelles, on peut voir que l'appel à la méthode m ne se fait pas via une étiquette.

Image non disponible

La ligne 9 trouve l'adresse de la vtable de l'objet. L'adresse de l'objet est placée sur la pile ligne 11. La ligne 12 appelle la méthode virtuelle en se branchant à la première adresse dans la vtable(45). Cet appel n'utilise pas d'étiquette, il se branche à l'adresse du code sur lequel pointe EDX. Ce type d'appel est un exemple de liaison tardive (late binding). La liaison tardive repousse le choix de la méthode à appeler au moment de l'exécution du code. Cela permet d'appeler la méthode correspondant à l'objet. Le cas normal (Figure 7.20) code en dur un appel à une certaine méthode et est appelé liaison précoce (early binding), car la méthode est liée au moment de la compilation.

Le lecteur attentif se demandera pourquoi les méthodes de classe de la Figure 7.21 sont explicitement déclarées pour utiliser la convention d'appel C en utilisant le mot-clé __cdecl. Par défaut, Microsoft utilise une convention différente de la convention C standard pour les méthodes de classe C++. Il passe le pointeur sur l'objet sur lequel agit la méthode via le registre ECX au lieu d'utiliser la pile. La pile est toujours utilisée pour les autres paramètres explicites de la méthode. Le modificateur __cdecl demande l'utilisation de la convention d'appel C standard. Borland C++ utilise la convention d'appel C par défaut.

Observons maintenant un exemple légèrement plus compliqué (Figure 7.23).

Image non disponible

Dans celui-là, les classes A et B ont chacune deux méthodes : m1 et m2. Souvenez-vous que comme la classe B ne définit pas sa propre méthode m2, elle hérite de la méthode de classe de A. La Figure 7.24 montre comment l'objet b apparaît en mémoire.

Image non disponible

La Figure 7.25 montre la sortie du programme.

Image non disponible

Tout d'abord, regardons l'adresse de la vtable de chaque objet. Les adresses des deux objets B sont les mêmes, ils partagent donc la même vtable. Une vtable appartient à une classe pas à un objet (comme une donnée membre static). Ensuite, regardons les adresses dans les vtables. En regardant la sortie assembleur, on peut déterminer que le pointeur sur la méthode m1 est au déplacement 0 (ou dword 0) et celui sur m2 est au déplacement 4 (dword 1). Les pointeurs sur la méthode m2 sont les mêmes pour les vtables des classes A et B, car la classe B hérite de la méthode m2 de la classe A.

Les lignes 25 à 32 montrent comment l'on pourrait appeler une fonction virtuelle en lisant son adresse depuis la vtable de l'objet(46). L'adresse de la méthode est stockée dans un pointeur de fonction de type C avec un pointeur this explicite. D'après la sortie de la Figure 7.25, on peut voir comment cela fonctionne. Cependant, s'il vous plaît, n'écrivez pas de code de ce genre ! Il n'est utilisé que pour illustrer le fait que les méthodes virtuelles utilisent la vtable.

Il y a plusieurs leçons pratiques à tirer de cela. Un fait important est qu'il faut être très attentif lorsque l'on écrit ou qu'on lit des variables de type classe depuis un fichier binaire. On ne peut pas utiliser simplement une lecture ou une écriture binaire, car cela lirait ou écrirait le pointeur vtable depuis le fichier ! C'est un pointeur sur l'endroit où la vtable réside dans la mémoire du programme et il varie d'un programme à l'autre. La même chose peut arriver avec les structures en C, mais en C, les structures n'ont des pointeurs que si le programmeur en définit explicitement. Il n'y a pas de pointeurs explicites déclarés dans les classes A ou B.

Une fois encore, il est nécessaire de réaliser que des compilateurs différents implémentent les méthodes virtuelles différemment. Sous Windows, les objets de la classe COM (Component Object Model) utilisent des vtables pour implémenter les interfaces COM(47). Seuls les compilateurs qui implémentent les vtables des méthodes virtuelles comme le fait Microsoft peuvent créer des classes COM. C'est pourquoi Borland utilise la même implémentation que Microsoft et une des raisons pour lesquelles gcc ne peut pas être utilisé pour créer des classes COM.

Le code des méthodes virtuelles est identique à celui des méthodes non virtuelles.

Seul le code d'appel est différent. Si le compilateur peut être absolument sûr de la méthode virtuelle qui sera appelée, il peut ignorer la vtable et appeler la méthode directement (p.e., en utilisant la liaison précoce).

VIII-B-6. Autres fonctionnalités C++

Le fonctionnement des autres fonctionnalités du C++ (p.e., le RunTime Type Information, la gestion des exceptions et l'héritage multiple) dépasse le cadre de ce livre. Si le lecteur veut aller plus loin, The Annotated C++ Reference Manual de Ellis et Stroustrup et The Design and Evolution of C++ de Stroustrup constituent un bon point de départ.

IX. Annexe A : Instructions 80x86

IX-A. Instructions hors virgule flottante

Cette section liste et décrit les actions et les formats des instructions hors virgule flottante de la famille de processeurs Intel 80x86.

Les formats utilisent les abréviations suivantes :

Image non disponible

Elles peuvent être combinées pour les instructions à plusieurs opérandes. Par exemple, le format R,R signifie que l'instruction prend deux opérandes de type registre. Beaucoup d'instructions à deux opérandes acceptent les mêmes opérandes. L'abréviation O2 est utilisée pour représenter ces opérandes : R,R R,M R,I M,R M,I. Si un registre 8 bits ou un octet en mémoire peuvent être utilisés comme opérande, l'abréviation R/M8 est utilisée.

Le tableau montre également comment les différents bits du registre FLAGS sont modifiés par chaque instruction. Si la colonne est vide, le bit correspondant n'est pas modifié du tout. Si le bit est toujours positionné à une valeur particulière, un 1 ou un 0 figure dans la colonne. Si le bit est positionné à une valeur qui dépend des opérandes de l'instruction, un C figure dans la colonne. Enfin, si le bit est modifié de façon indéfinie, un « ? » figure dans la colonne. Comme les seules instructions qui changent le drapeau de direction sont CLD et STD, il ne figure pas parmi les colonnes FLAGS.

Image non disponible
Image non disponible
Image non disponible
Image non disponible
Image non disponible
Image non disponible

IX-B. Instruction en virgule flottante

Dans cette section, la plupart des instructions du coprocesseur mathématique du 80x86 sont décrites. La section description décrit brièvement l'opération effectuée par l'instruction. Pour économiser de la place, il n'est pas précisé si l'instruction décale la pile ou non.

La colonne format indique le type d'opérande pouvant être utilisé avec chaque instruction. Les abréviations suivantes sont utilisées :

Image non disponible

Les instructions nécessitant un Pentium Pro ou supérieur sont signalées par un astérisque (*).

Image non disponible
Image non disponible
(1)
L'indice 2 est utilisé pour indiquer que le nombre est représenté en binaire, pas en
décimal.
(2)
S'il n'est pas clair que le point de départ est important, essayez de convertir l'exemple en partant de la gauche.
(3)
En fait, l'ASCII n'utilise que les 7 bits les plus faibles et donc n'a que 128 valeurs à utiliser.
(4)
Cependant, ils peuvent également utiliser une interface de plus bas niveau au niveau du noyau.
(5)
Les nombres à virgule flottante en simple précision sont équivalents aux variables float en C.
(6)
TWORD définit une zone mémoire de 10 octets. Le coprocesseur virgule flottante utilise ce type de données.
(7)
Le fichier asm_io.inc (et le fichier objet asm_io que requiert asm_io.inc) sont dans les exemples de code à télécharger sur la page web de ce tutoriel, http://www.drpaulcarter.com/pcasm.
(8)
Le Chapitre III parle de ce registre.
(9)
GNU est un projet de Free Software Foundation (http://www.fsf.org).
(10)
http://www.delorie.com/djgpp.
(11)
Il est courant de penser que les fichiers ont un caractère EOF comme dernier caractère. Ce n'est pas vrai !
(12)
La raison de cette nécessité sera exposée plus tard.
(13)
2 est le seul nombre premier pair.
(14)
Cet opérateur est différent des opérateurs && binaire et & unaire !
(15)
Application Programming Interface, Interface pour la Programmation d'Applications
(16)
Signie Portable Operating System Interface for Computer Environments, Interface Portable de Système d'Exploitation pour les Environnements Informatiques. Un standard développé par l'IEEE basé sur UNIX.
(17)
En fait, un paramètre de type mode_t qui est défini comme entier par un typedef.
(18)
En fait, passer d'une représentation à l'autre pour entier consiste à inverser l'ordre des octets ; donc, convertir de little vers big ou de big vers little est la même opération. Donc, ces deux fonctions font la même chose.
(19)
En fait, il est également possible d'empiler des mots, mais en mode protégé 32 bits, il est mieux de ne travailler qu'avec des doubles-mots sur la pile.
(20)
Il est autorisé d'ajouter une constante à un registre lors de l'utilisation de l'adressage indirect. Des expressions plus complexes sont également possibles. Ce sujet est traité dans le chapitre suivant.
(21)
GAS est l'assembleur GNU que tous les compilateurs GNU utilisent. Il utilise la syntaxe AT&T qui est très différente des syntaxes relativement similaires de MASM, TASM et NASM.
(22)
Le compiltateur C Watcom est un exemple qui n'utilise pas les conventions standard par défaut. Voyez les sources exemple pour Watcom pour plus de détails.
(23)
Un programme multithreadé a plusieurs threads d'exécution. C'est-à-dire que le programme lui-même est multitâche.
(24)
Positionner AH à zéro équivaut à supposer implicitement que AL est un nombre non signé. S'il est signé, l'action appropriée serait d'insérer une instruction CBW entre les lignes 6 et 7.
(25)
On peut indiquer une taille, mais elle n'est pas prise en compte par le compilateur.
(26)
Une autre complication est qu'on ne peut pas copier la valeur du registre DS dans ES directement en utilisant une instruction MOV. À la place, la valeur de DS doit être copiée dans un registre universel (comme AX) puis être copié depuis ce registre dans ES, ce qui utilise deux instructions MOV.
(27)
Un préfixe d'instruction n'est pas une instruction, c'est un octet spécial qui est placé avant une instruction de chaîne qui modifie son comportement. D'autres préfixes sont également utilisés pour modifier le segment par défaut des accès mémoire.
(28)
Le type long double de certains compilateurs (comme celui de Borland) utilise cette précision étendue. Par contre, d'autres compilateurs utilisent la double précision à la fois pour les double et long double (c'est autorisé par le C ANSI).
(29)
La seule différence est que pour l'infini et les valeurs indéfinies, l'exposant décalé vaut 7FF et non pas FF.
(30)
La racine d'une fonction est une valeur x telle que f(x) = 0.
(31)
Cependant, le 80486SX n'avait pas de coprocesseur intégré. Il y avait une puce 80487SX pour ces machines.
(32)
Voyez le chapitre sur la gestion de la mémoire virtuelle de n'importe quel livre sur les Systèmes d'Exploitation pour une explication de ce terme.
(33)
En fait, le standard ANSI/ISO C laisse une certaine liberté au compilateur sur la façon d'organiser les bits. Cependant, les compilateurs C courants (gcc, Microsoft et Borland) organisent les champs comme cela.
(34)
Small Computer Systems Interface, un standard de l'industrie pour les disques durs, etc.
(35)
Mélanger différents types de champs de bits conduit à un comportement très étrange ! Le lecteur est invité à tester.
(36)
La correspondance n'a pas à être exacte, le compilateur prendra en compte les correspondances trouvées en transtypant les arguments. Les règles de ce procédé sont en dehors de la portée de ce livre. Consultez un livre sur le C++ pour plus de détails.
(37)
Bien sûr, il faudrait déclarer la fonction avec une édition de liens en C, comme nous en a7vons parlé dans la Section IIX.B.1.
(38)
Les compilateurs supportent souvent cette fonctionnalité comme une extension du C ANSI.
(39)
Souvent appelés fonctions membres en C++ ou plus généralement méthodes.
(40)
Le compilateur gcc inclut son propre assembleur appelé gas. L'assembleur gas utilise la syntaxe AT&T et donc le compilateur produit un code au format gas. Il y a plusieurs sites sur le web qui expliquent les différences entre les formats INTEL et AT&T. Il existe également un programme gratuit appelé a2i (http://membres.multimania.fr/placr/a2i.html), qui passe du format AT&T au format NASM.
(41)
Comme d'habitude, rien n'est caché dans le code assembleur !
(42)
Pourquoi ? Car les opérations d'addition commenceront ainsi toujours par le début du tableau et avanceront.
(43)
Voyez le code source d'exemple pour obtenir le code complet de cet exemple. Le texte ne se réfèrera qu'à certaines parties du code.
(44)
Pour les classes sans méthode virtuelle, les compilateurs C++ rendent toujours la classe compatible avec une structure C normale qui aurait les mêmes données membres.
(45)
Bien sûr, la valeur est déjà dans le registre ECX. Elle y a été placée à la ligne 8 et la ligne 10 pourrait être supprimée et la ligne suivante changée de façon à empiler ECX. Le code n'est pas très efficace, car il a été généré en désactivant les optimisations du compilateur.
(46)
Souvenez-vous, ce code ne fonctionne qu'avec les compilateurs MS et Borland, pas gcc.
(47)
Les classes COM utilisent également la convention d'appel __stdcall, pas la convention C standard.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Paul A. Carter. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique Assembleur : Alcatîz -
  • Nous contacter
  • Participez
  • Hébergement
  • Informations légales

Copyright © 2000-2016 - www.developpez.com


MASM librairies

Autres Sujets

Mettre a jour les librairies masm32
Utiliser les DLL
Pourquoi utiliser Statiquement ou dynamiquement les dll ?

Créer une librairie a partir de ses définitions de prototypes
 Utiliser le C++ et MASM

MASM32 fournit les moyens de se constituer une librairie personnelle.Cela permet d'intégrer facilement dans n'importe quel programme ses propres routines les plus souvent utilisées.En cas de debuggage de ces mêmes routines,il suffit alors de recompiler les programmes qui les utilisent.Inconvénient: les fonctions doivent être documentées avec la même rigueur que winhelp , prendre winhelp comme modèle.

La marche a suivre est la suivante:

créer un répertoire (c:\masm32\persolib)

placer make.bat dans ce répertoire

;----------------------------------- début fichier make.bat----------------------------------------------------------

;----------------------------------- fin de fichier make.bat ----------------------------------------------------------

L'instruction "pause" n'est pas optionnel.Windows fonctionnant en multitache,multithread , commence par éxécuter le link puis passe à la suppression des fichiers objets.La suppression est beaucoup plus rapide que link,le résultat est que la librairie produite est vide.L'éxécution asynchrone des fichiers batchs ressemble a un bug windows.

Former le Code du fichier Objet

Insérer un PROC dans le fichier suivant

    ;------------------------------------------------Squelette de création de l'objet----------------------

    ; #########################################################################

    .386 ; force 32 bit code

    .model flat, stdcall ; memory model & calling convention

    option casemap :none ; case sensitive

    ; --------------------------------

    ; Les fichiers inclus içi ,suprimer les points virgules:NE déclarer AUCUNE LIBRAIRIE

    ;c'est au programme utilisateur de rajouter les .lib nécessaires.

    ; --------------------------------

    include \masm32\include\windows.inc

    include \masm32\include\lz32.inc

    include \masm32\include\user32.inc

    include \masm32\include\kernel32.inc

    ; include \masm32\include\masm32.inc

    ; include macros.txt

    ; ---------------------------------------------------------

    ; Les PROTO sont tous regroupés dans un fichier .inc du même

    ; nom que la librairie .lib

    ; ---------------------------------------------------------

    ;lorsque un proc en utilise un autre,syntaxe externdef nom : définition

    ; EXTERNDEF LongZtexte:PROTO :DWORD,:DWORD

    ; EXTERNDEF EtatMemoirePile :MEMORY_BASIC_INFORMATION

    ; EXTERNDEF PoriginePile:DWORD

    .data ;les données placées içi ne seront reconnus que par cette routine

    .code

    ;################################################################

    ;insérer le PROC içi

    ;################################################################

    end ;et non pas end start,signe le fichier librairie

    ;---------------------------- fin de fichier ----------------------------------------------

assembler chaque proc séparément pour vérifier son fonctionnement
Procéder de même pour chaque PROC

Lancer MAKE, fini !

          Documenter la fonction en utilisant le HTML Help Wokshop

Mise à jour des librairies de MASM32

Trois utilitaires permettent de gérer les problèmes liés aux librairies

inc2l,malgré quelques défauts,extrait la librairie d'une dll lorsqu'on lui fournit un fichier inclus comportant les déclarations de PROTO des fonctions fournis par la DLL.Voir MSDN pour écrire les nouveaux protos,on peut se servir de l'ancien .inc pour éviter de tout réécrire.Les nouvelles définitions de protos doivent être exacts et comporter la taille des arguments passés a l'appel de la fonction.On ne peut pas se simplifier le travail en ne mettant que le nom de la fonction.Mettre l'inclus dans un répertoire et appeler inc2l en lui passant le nom de l'inclus

l2inca: Extrait les protos d'une librairie,mieux vaut utiliser protolib (editmasm)

l2incu:même chose que l2inca mais pour la convention UNICODE ,fonctions se terminant par W

LIB:création de la librairie a partir des fichiers objets

Utilisation statique et dynamique des DLL (adresses des API fixes ou recherchées)

De manière statique:

    On rajoute les déclarations de proto et la librairie aux sources

    include \MASM32\include\dbghelp.inc

    includelib \masm32\lib\dbghelp.lib

    Utilisation des fonctions

invoke SymSetOptions,options

De manière dynamique: (voir la page outils)

    Aucun fichier a inclure au source

    Déclarations:

      zSymSetOptions TYPEDEF PROTO :DWORD

      fSymSetOptions TYPEDEF PTR zSymSetOptions

      SymSetOptions TEXTEQU <fSymSetOptions ptr HSymSetOptions>

    data

      HSymSetOptions dd 0

    Charger la dll

      invoke LoadLibrary,SADD("dbghelp.dll")

    Obtenir l'adresse de la fonction

    invoke GetProcAddress,hLib,SADD("SymSetOptions")

    mov HSymSetOptions,eax

    Utilisation des fonctions

    invoke SymSetOptions,options

Mises A jour des librairies Windows:

Utilisation Statique:(adresse des APIS fixes)

Lorsque microsoft modifie les dll,les librairies sont elles aussi modifiées.Les adresses des fonctions APIS changent et ..... les programmes utilisant les vieilles librairies ne marchent plus.Il est nécessaire alors de recompiler avec les nouveaux inclus et les nouvelles librairies.

Utilisation Dynamique:

Plus de problèmes.L'adresse étant recherché dans la dll,celle ci peut avoir changée , cela n'a pas d'importance.

Microsoft fait attention au problème et recommande l'utilisation dynamique lorsque les APIS sont en cours de dévellopement et sont susceptibles de changer

Exemples:

dbghelp.dll , la dll des debugging tools change tous les six mois,utiliser la de manière dynamique

DirectX est en constante évolution , a utiliser dynamiquement

Les fonctions serveurs 2003 sont elles aussi susceptibles d'être modifiés,a utiliser dynamiquement de préfèrence

Kernel32.dll est a la base de tous les programmes,on peut utiliser le mode statique.

Exemple d'utilisation dynamique

Un exemple un peu poussé puisqu'il n'utilise que le chargement dynamique.C'est editmasm qui a créé les nombreuses déclarations nécessaires à l'aide des déclarations de protos contenus dans masm32

Bricolage:

Comment créer une librairie a partir d'une dll

Recréer les librairies

deux méthodes

Avec DUMPBIN et LIB /DEF:

1) dumpbin /EXPORTS nom.dll > nom.txt ,extrait le nom des fonctions exportés par la dll

2) créer le fichier def (celui servant a définir les fonctions exportés

lorsque l'on crée une dll)

LIBRARY nom

EXPORTS

;suite des noms repris de nom.txt

3) créer la librairie

lib /DEF:nom.def

La librairie créer ne peut être appeler que par un call

Créer le code d'une librairie sans code,ne contenant que l'appel

    ;----------------------------------------------------------

    .386

    .model flat,stdcall

    .code

    fonction1 proc param1:DWORD, param2:DWORD ...

    fonction1 endp

    ........2 et ainsi de suite

    end

    ;----------------------------------------------------------

    c'est le code d'une librairie,pas de "start: end start"

    créer le point def comme précédemment

    compiler

    ml /c /coff /Cp nom.asm

    link /DLL /NOENTRY /def:nom.def /subsystem:windows nom.obj

    on obtient une lib invokable, une dll vide de code,a effacer

Utiliser polib

C:\masm32\tools\INC2L\INC2L.EXE efface la dll

Le principe de fonctionnement est expliqué au dessus

Une librairie kernel a jour est nécessaire pour utiliser les exceptions avec editmasm
Préférez les librairies fournies par microsoft,quelques problèmes existent en les recréant.

On peut avoir des librairies a jour avec le SDK

Utiliser le C++ et MASM ,résoudre les problèmes d'incompatibilité avec d'autres systèmes

On peut utiliser des librairies C++ dans des programmes MASM.

Libcmt.lib vous oblige à modifier la structure habituelle de MASM et a utiliser un mot réservé

.code

start:

---- içi code--

end start

devient

.code

WinMain proc STDCALL public uses esi edi ebx,hInst:DWORD,hPrev:DWORD,\
lpCmdLine:DWORD,nShowCmd:SDWORD

WinMain endp
end

WinMain est le mot réservé pour désigner le point d'entrée du code (au lieu de start:,changeable).Le fichier crt0.c donne les mots réservés à utiliser en fonction du programme.Celui ci se trouve en C:\Program Files\Microsoft SDK\src\crt.

msvcrt.lib vous oblige à utiliser un manifest.Un fichier manifest est un fichier texte,modifiable avec notepad,dont le format d'écriture est le XML.

Manifest:

Un fichier manifest est identifié comme tel par le système,lorsqu'il possède l'extension .manifest.Il sert a aiguiller le système dans l'utilisation de tel ou tel librairie.Il joue un rôle particulier lorsqu'on utilise CRT,MFC et ATL.Il indique au système ou trouver les librairies Mt.exe est le tool permettant de les manipuler (SDK et C++).Le cas général,sans problème particulier,est résolu par celui-ci:

Il doit être présent dans le fichier ressource pour être opérationnel

#define RT_MANIFEST 24

#define APP_MANIFEST 1

APP_MANIFEST RT_MANIFEST DISCARDABLE "libs.manifest" //"Microsoft.VC80.CRT.manifest"

Au cas ou un programme doit utiliser une ancienne version de dll,le fichier manifest sert à déclarer la dépendance du programme vis à vis de ces DLL.Exemple le Microsoft.VC80.CRT.manifest.les librairies dépendantes doivent se trouver dans le même répertoire que le programme

Un fichier manifest peut aussi servir à déclarer l'utilisation des XP THEMES,common controls

Si vous avez deux .manifest à incorporer (un seul est possible) utiliser mt.exe pour les réunir.


La PILE et la création d'un PROC

Masm gère la pile a votre place , mais ce n'est pas pour autant qu'il faut l'ignorer.Windows s'occupe de l'espace alloué a la pile , ce qui rend un débordement de pile (Stack fault) peu probable.

Au démarrage,la pile se voit affecter un tampon de mémoire paginer(page = bloc de 4096 octets).Pour obtenir la valeur de esp,on prend le pointeur de début de tampon et on lui ajoute la taille de mémoire réservée.Chaque push va décrémenter esp de 4,chaque pop incrémenter esp de 4.

MASM arrondi le push d'un byte,d'un word en DWORD (4bytes).Les variables locales (en pile) prennent la valeur du contenu de la pile.

Toutes les adresses de la pile sont alignées à 4

MASM permet l'utilisation de MACRO pour créer son propre STACKFRAME (constitution du proc)

Il n'est plus nécessaire de réserver de l'espace mémoire à la pile comme avec les 8086.L'espace alloué à la pile est ici en bleu,en rouge les zones de crash..La règle est ,premier entré,dernier sorti.

push eax

push ecx ;dernier entré dans la pile

pop ecx ;premier sorti

pop eax

Par défaut l'espace alloué a la pile est de 4096 bytes.Pour l'agrandir,l'utilisation de .STACK n'est d'aucune utilité.

Au link on peut utiliser /STACK:0x100000,0x5000

Avec JWASM,ML 8,dans le source

option dotname

.drectve segment info

db "-stack:0x100000,0x5000 "

.drectve ends

Pour exemple réutilisons le projet test , précédemment créer

Créons un PROC.

Avec editmasm utiliser 'code' 'nouveau PROC'.
Les paramètres du proc seront les premiers à être placés en pile.Ensuite viennent l'adresse de retour,ebp et les variables locales.

;#############################################

Appel PROTO :DWORD

Appel PROC :DWORD

Local :DWORD

Local [256]:BYTE

FindeAppel:

ret

Appel endp

;#############################################

Editmasm produit les lignes ci dessus.La déclaration du proto doit être placé tel que dans la section déclaration par copier coller, en dehors des segments code et data.

Il faut ensuite donner un nom aux variables et dimensionner la chaîne

Appel PROC Nombre:DWORD

Local resultat:DWORD

Local chaine[8]:BYTE

Un byte= 8 bits , un DWORD = 32 bits

On peut l'appeler immédiatement,taper ces lignes juste apès start

mov edx,esp

invoke appel,edx

compiler,une erreur se produit A majuscule dans Appel .

'tout faire' 'executer' le proc ne fait rien.(normal)

A l'appel ,les variables passés au proc sont poussés en pile,Ensuite vient l'adresse de retour(chargement de EIP au moment du retour) .A l'entrée dans le proc,EBP est chargé en pile , prend la valeur de ESP et pointe l'adresse de retour.De l'espace est alloué en pile pour les variables locales et ESP pointe en fin de chaine.

En fin de proc , l'instruction leave libere la pile des variables locales ,le ret récupère l'adresse de retour et vide la pile des variables passées a l'appel ( ret Nb) .EBP et ESP joue un rôle majeur au momment du retour.On veillera donc à leur état avant de mettre fin au proc. Tout ceci est géré par masm.

C'est ce que l'on peut vérifier en insérant quelques lignes dans le proc.

Pour voir la valeur des registres il nous faut un convertisseur hexa to ASCII (texte) et une messagebox.MASMLIB (bibliothèque de routine) fournit la première.Le bloc note fournit les lignes nécessaires a une messagebox ainsi 'écrire dans la barre d'état',servons nous en.

invoke dwtoa,lParam,addr ZoneMessagesErreurs

;invoke dw2hex,lParam,addr ZoneMessagesErreurs

INVOKE SendMessage, Hstatus, SB_SETTEXT, 2, addr ZoneMessagesErreurs

La troisieme ligne s'adresse a la barre de statut (Hstatus) et est inutile,suprimons la . Reste une conversion dw(hexa) a ascii en décimale et l'autre en ascii hexa.En changeant le nom du fichier d'aide appelé en masmlib dans la boite F1,on obtient des infos sur dwtoa et dw2hex.Gardons la première en l'adaptant.

invoke dwtoa,resultat,addr chaine

l'instruction va prendre une valeur hexa dans résultat et inscrire sa valeur décimale texte dans chaine.chargeons résultat.

mov eax,esp

mov resultat,eax

invoke dwtoa,resultat,addr chaine

affichons la chaine a l'écran,copie des lignes messagebox et changement de phrase par chaine,titre ESP

InsTxt TITRE000,"ESP entrée dans proc"

invoke MessageBox,NULL,ADDR chaine,ADDR TITRE000,MB_OK

Pour avoir esp avant l'appel,faisons comme suit,listing complet

mov edx,esp

invoke Appel,edx

invoke MessageBox,0,addr szMsg,addr szDlgTitle ,40h

call ExitProcess

;######################################

Appel PROC Nombre:DWORD

Local resultat:DWORD

Local chaine[8]:BYTE

mov eax,esp

mov resultat,eax

invoke dwtoa,resultat,addr chaine

InsTxt TITRE000,"ESP à l'entrée du proc"

invoke MessageBox,NULL,ADDR chaine,ADDR TITRE000,MB_OK

invoke dwtoa,Nombre,addr chaine

InsTxt TITRE001,"ESP avant l'appel"

invoke MessageBox,NULL,ADDR chaine,ADDR TITRE001,MB_OK

FindeAppel:

ret

Appel endp

;####################################

La valeur 4 après le ret correspond à la taille en octets (byte) de 'Nombre'.La soustraction des deux valeurs affichées donne 24 bytes soit 6 DWORD.L'espace pile nécessaire à l'appel est donc de six DWORD.Avec un registre de 32 bits on peut adresser plus de 2 Go de mémoire.Kernel se réserve la fraction au dessus des deux gigas

sachant ou pointe EBP à l'entrée du proc,comment peut on retrouver Nombre en passant par EBP ? (la question a 100 balles)

mov eax,[EBP+8]

Initialiser des variables Locales:

La création d'une variable en pile se fait sans initialiser la mémoire.MASM se contente de réserver la place nécessaire et c'est tout.Avant d'appeler une API en lui passant une structure créée en pile,il faut s'assurer que toutes les données de la structure soient correctement initialiser.Le meilleur moyen pour cela est d'initialiser toutes les données de la structure à zéro à l'aide de la macro ZEROLOCALES.

ZEROLOCALES MACRO dernierelocale:REQ

mov edx,edi ;conserve edi

mov ecx,ebp

lea edi,dernierelocale ;preserve uses mis après locales

sub ecx,edi ;ecx = taille totale des locales

xor eax,eax

shr ecx,2 ;divise par 4 (bytes)

rep stosd ;rempli de zéro

mov edi,edx ;rétablit edi

ENDM

On passe le dernier nom de locale à la macro et celle ci initialise toutes les locales à zéro.

La macro utilise les propriétés de construction d'un proc par masm.c'est à dire.

EBP est la valeur de ESP à l'entrée du proc

Le flag de direction est a zero ( le préfixe rep incrémente les adresses)

Les locales ne sont constituées que de DWORD.Toute taille de variable est arrondi au dword supérieur

Vérifier la taille de la pile dans l'éxécutable

Sans se lancer dans l'étude du format PE ,on peut vérifier dans l'éxécutable la taille de la pile.

Il faut charger le fichier en mémoire.On peut lancer un programme qui charge en mémoire son propre fichier .exe.

invoke ChargerFichierMem,addr fichier,addr memoire

mov ebx,memoire.Mpoint

;ebx pointe en 0 du tampon mémoire créé par globalalloc

mov edx,(IMAGE_DOS_HEADER ptr [ebx]).e_lfanew
;on rajoute e_lfanew au pointeur pour trouver l'adresse de IMAGE_NT_HEADERS

add ebx,edx

mov edx, (IMAGE_NT_HEADERS ptr [ebx]).OptionalHeader.SizeOfStackCommit

mov COMMIT,edx ;4096 bytes par défaut

mov edx, (IMAGE_NT_HEADERS ptr [ebx]).OptionalHeader.SizeOfStackReserve 1 048 576 bytes,256 pages

Voila,on est en possession du montant de mémoire alloué a la pile par défaut,COMMIT et du maximum de la taille atteignable par la pile et on peut vérifier si nos instructions fonctionnent.
.STACK échoue misérablement.

L'option de link fonctionne normalement et affecte le premier chiffre au maximum de mémoire et le second à l'utilisable

Le problème est de savoir comment va se comporter le micro en cas de dépassement de la taille allouée.En procédant à une succession de push dans la pile,on s'aperçoit que l'on peut largement dépasser le montant alloué (4096) sans pour autant provoquer de plantage.

On court au débordement de pile , soit lorsqu'on l'utilise comme un stockage de données soit lorsque l'on fait de la réentrance.Le proc s'appelle lui même
On peut augmenter la taille de la pile en cours de programme , en lui rajoutant des pages (4096 bytes)

Référence sur le PE file

ALLOCATION DYNAMIQUE de la PILE

La pile utilise de la mémoire paginée (page de 4096 octets).En cas de débordement (écritutre dans une zone non attribuée) le système gère _XCPT_GUARD_PAGE_VIOLATION ,si la mémoire réservée est suffisante,le système alloue une page suplémentaire à la pile.Une page et UNE SEULE,si la demande du proc excède 1 page,access violation.

Voici la méthode:

StackSize PROC

ASSUME FS:Nothing

mov ecx,0

mov edx,FS:[ecx].NT_TIB.StackLimit ;FS:[8]

mov eax,FS:[ecx].NT_TIB.StackBase ;FS:[4]

sub eax,edx ;allocated stack

ASSUME FS:ERROR

ret

StackSize endp

ecrire un dword a StackLimit,vérifier que la pile croit d'une page.

La série des fonctions en virtual permet d'allouer cette mémoire.LES STRUCTURES UTILISEES DOIVENT AVOIR UN ALIGNEMENT DE 4.

En lui passant la valeur de ESP ,on a accès aux renseignements concernant le bloc mémoire a partir de la page contenant esp jusqu"aux pages situées en sommet du bloc.Cela veut dire que si bloc continue vers les adresses basses,il faut descendre le pointeur d'une page pour vérifier la continuité du bloc et ainsi de suite.

Remarques pratiques

VirtualQuery fourni:

BaseAddress adresse le début de la page pour le pointeur fourni,en adressage linéaire.Le pointeur se situe entre ce début +1 page(4096 octets)

AllocationBase début du bloc de pages allouées ,en adressage linéaire.A ne pas confondre avec l'offset 0 du pointeur.AllocationBase + RegionSize = adresse linéaire de fin du tampon

adressage linéaire:07770128h (un plan mémoire avec un éventuel décalage)
adressage virtuel : CS:401560h
on peut fournir a Virtualquery les deux types d'adresses en omettant le segment pour le virtuel.

VirtualLock

Utilisable moyennant beaucoup de précautions et de contraintes en termes de performances systèmes.Un programme ne peut verrouiller que 30 pages.En cas de demandes supérieur,les résultats sont inattendus.La fonction empêche l'envoi de la mémoire sur le disque + autres.

VirtualAlloc

Peut allouer de la mémoire paginée par bloc entier ou au page a page par mesure d'économie.Sa principale particularité est de pouvoir incorporer un bloc alloué a un bloc non alloué sans détruire le bloc alloué.le bloc non alloué est rempli de zéros.Cela permet d'augmenter et de diminuer l'espace mémoire de la pile sans déplacer esp.Le bloc mémoire peut ainsi varier de 1 page a Maxpage (256 par défaut) sans problème.Si le maximum de pages par défaut ne suffit pas,il faut modifier les options de link comme expliqué plus haut.

L'EXEMPLE

Tout commence par la lecture page par page de la mémoire pile pour établir la continuité du bloc de mémoire.Ceci fait,on procède a l'augmentation ou a la diminution du bloc le tout sans modifier esp ce qui rend le procéder parfaitement fiable.a n'importe quel moment on peut augmenter ou diminuer la taille de la mémoire.La lecture du bloc permet , entre autre choses,de mettre en évidence le rôle du sytème au démarrage.Il rajoute des pages pour lancer le WndProc.

grospile est la pour consommer de la mémoire pile et tester le critère de non débordement de la pile.Il se rappelle lui même et tient un compte du nombre d'appels successifs.Le nombre d'appels enregistrés dépend évidemment du nombre de pages demandés.Une aproximation a eclaircir ,rajouter , pDispatch: dword a la définition de handler.

L'exemple utilise ma bibliothèque perso pour retrouver les messages d'erreurs.Elle est disponible sur ce site.

STACKFRAME:

On peut créer ses propres méthodes d'appel des proc en utilisant les instructions du µp.

OPTION PROLOGUE:NONE

OPTION EPILOGUE:NONE

align 16

fail:

xor eax, eax

ret 4

align 16

isupper proc char:BYTE

cmp BYTE PTR [esp+4], 65 ; test if below "A"

jb fail

cmp BYTE PTR [esp+4], 90 ; test if above "Z"

ja fail

mov eax, 1

ret 4

isupper endp

OPTION PROLOGUE:PrologueDef

OPTION EPILOGUE:EpilogueDef

Utiliser le listing pour voir ce que produit masm avec un proc standard et un proc sans cadre constitutif.

 | Dword -3
 | Dword -2
 | Dword -1
MAX ESP | Dword 0
 | Dword 1
 | Dword 2
 | Dword 3
 | Dword 4
 | Dword 5
 | Dword 6
 | Dword 7
 | Dword 8
 | Dword 9
 | Dword 10
0 ESP | Dword 11

zone de Débordement de pile

Nouvel état de la pile , après l'appel au proc.En vert l'espace utilisée par le proc

 | Dword -3
 | Dword -2
 | Dword -1
 | Dword 0
 | Dword 1
 | Dword 2
 | Dword 3
 | Dword 4
 | Dword 5
chaine[1] | Dword 6
chaine[2] | Dword 7
resultat | Dword 8
Valeur EBP | Dword 9
adresse retour | Dword 10
paramètre appel fonction | Dword 11

rajout masm32

entrée proc

push ebp

mov ebp,esp

add esp,0FFFFFFF4h;-12

L'espace mémoire réservé pour les variables locales est dans un état indéfini,non zéro

à la sortie du proc

mov eax,esp

mov [ebp-4],eax

leave

ret 4


Introduction au langage 'assembleur' (Intel en particulier)

Introduction au langage "assembleur" (Intel en particulier)

Table des matières

  • I. Introduction
  • II. Commençons par le vocabulaire
  • III. Liste des registres
  • IV. Liste des drapeaux
  • V. Liste des instructions
  • VI. Liste des interruptions
  • VII. Exemples concrets
    • VII-A. Lecture d'une touche au clavier
    • VII-B. Effacement de l'écran dans le mode VGA (320x200 pixels en 256 couleurs)

J'ai écris cette « introduction » à l'assembleur assez rapidement, et croyant que vous vous y connaissiez un peu dans ce domaine. Alors si vous trouvez une erreur dans ce cours, ou quelque chose vous est incompréhensible, écrivez-moi : vous m'aiderez pour mon cours, et vous aiderez les prochains lecteurs de ce cours.

I. Introduction

Le processeur d'un ordinateur ne comprend aucun langage de programmation ni Turbo Pascal, ni assembleur, ni même C++ ou Java (du moins aucun des processeurs compatibles Intel 80x86). Il ne connait qu'une chose : le langage machine. C'est une liste de nombre 8 bits en hexadécimal du style : "B0h 12h", pas très parlant. L'assembleur c'est la version « humaine » du code machine, pour notre exemple celà donne : « mov al, 12h » (comprendre « copie la valeur 12h dans le registre AL »). Comme vous pouvez le constater, c'est le langage de programmation le plus proche du processeur (mise à part si vous arrivez à programmer en langage machine).

Question qui vous saute à l'esprit : à quoi ça sert ? L'intérêt premier est la vitesse, car c'est le langage avec lequel on peut faire les programmes les plus rapides. Le deuxième intérêt, qui n'est peut-être plus d'actualité aujourd'hui est l'accès aux interruptions DOS qui permettent un accès direct au matériel tel la souris, l'écran, ou la carte vidéo. Certains vétérants de la programmation n'utilisent que l'assembleur, certains pirates en particulier. Pourquoi ? Pour faire les plus petits programmes possibles, car le compileur ne fait que traduire l'assembleur en code machine (ce qu'on pourrait presque faire manuellement avec de l'entrainement). Et en même savoir exactement ce que contiendra notre fichier binaire (fichier exécutable, « .exe » ou « .com » sur les plate-formes Microsoft).

Personellement, je vous déconseille vivement du faire du 100% ASM (ASseMbleur), car on se perd vite dans le code source (qui atteind souvent plusieurs milliers de lignes car chaque instruction prend une ligne), et ce n'est pas très parlant comme langage. Je vous le recommande (uniquement) pour optimiser des fonctions déjà existantes dans votre langage de prédilection (Pascal, C++...) demandant une vitesse optimale, souvent pour les fonctions les plus souvent appelées ; tracé de ligne, copie d'un bloc mémoire, etc.

II. Commençons par le vocabulaire

Compilateur - Hexadécimal - Bit - Octet - Mot - Double-mot - Segment et offset - Interruption - Pile - Flags

  • Compilateur : Programme traduisant un code source (fichier texte, pour l'assembleur : fichiers portant l'extension « .asm ») en code machine, c'est à dire produisant un fichier binaire (ou fichier « exécutable », portant l'extension « .exe » ou « .com » sur les plate-formes Microsoft).
  • Hexadécimal : Dans la notation des nombres actuelle, on utilise les chiffres arabes compris entre 0 et 9, on a donc 10 chiffres, et le chiffre le plus grand est 9 (10-1). On dit de ce système de notation qu'il est en base 10 (décimal, déci = 10 en grec). Pour écrire 451 par exemple, on peut le décomposer comme 4*100 +5*10 +1*1 = 4*(10^2) +5*(10^1) + 1*(10^0).
    Le système hexadécimal au contraire est en base 16. Pour écrire 10 on utilise la lettre « a », 11 : « b », 12 : « c », 13 : « d », 14 : « e », et 15 : « f ». Donc par exemple 20 (16*1+4) s'écrira 14h. « h » est le suffixe des nombres hexadécimaux en assembleur, en Turbo Pascal on utilise le préfixe « $ » ($14) et en C le préfixe « 0x » (0x14). Pour convertir de l'hexadécimal en décimal, on procède à sa décomposition : 3DAh = 3*(16^2) +D*(16^1) +A*(16^0) = 3*256 +13*16 + 10*1 = 986.
    Remarque: Le processeur central (CPU : Central Processor Unit) travaille uniquement en binaire (uniquement avec bits), notation en base 2. Par exemple 9 s'écrira 1*8 +1 = 1*(2^3) +0*(2^2) +0*(2^1) +1*(2^0) = « 1001b » (« b » étant le suffixe des nombres binaires). Même le coprocesseur arithmétique qui calcule le cosinus et l'exposantiel travaille en binaire !
  • Bit :Un bit est une donnée pouvant prendre soit la valeur 0 (assimilée à false, faux en anglais) ou 1 (true, vrai en anglais). C'est l'unité du système binaire (voir la remarque de l'hexadécimal), un octet par exemple est un groupement de 8 bits.
  • Octet : Un octet est une suite de 8 bits. S'il est signé (+ ou -), il peut prendre une valeur comprise entre -128 et +127 (128 = 2 puissance (8-1) et 127 = 2 puissance (8-1) -1), car le premier bit est celui du signe (TRUE = « - ») et donc la valeur est en 7 bits. S'il n'est pas signé, il peut prendre une valeur comprise entre 0 et +255 (255 = 2^8 -1, « -1 » car le zéro est une valeur à part entière !).
  • Mot : Deux octets = 16 bits. Signé : valeurs entre -32768 (2^15) et +32767 (2^15 -1). Non signé : entre 0 à +65535 (2^16 -1).
  • Double-mot : Quatre octets = 2 doubles mots = 32 bits. Signé : valeurs de -2 147 483 648 (-2^31 = -2 147 millions) et +2 147 483 647 (2^31 -1), non-signé : entre 0 et +4 294 967 295 (quelques 4 294 millions).
  • Registre : Espace mémoire placé physiquement dans le coeur du processeur dans lequel sont stockés des valeurs. Les registres étaient au début codés sur 8 bits (un octet), ils étaient nommés « ?L » (où "?" peut être A,B,C ou D), puis ils sont passés en 16 bits (un mot) et deviennent « ?X ». Le truc magique est que si on modifie « ?L », on modifie la "partie basse" du mot « ?X », c'est à dire « ?X » est en faite composé d'un octet et de « ?L ». Puis avec l'arrivé du 386, on passe en 32 bits (double-mot) et les registres deviennent « E?X », là aussi, si on modifie « ?L » ou « ?X » on modifie une partie de « E?X ».
    Consultez la Liste des registres.
  • Segment et Offset : Les adresses mémoires (emplacement des octets dans les barrettes de mémoire) sont définies par deux registres : le segment (partie haute de l'adresse) et l'offset (partie basse de l'offset). On note l'adresse : « segment :[ offset ] » (« : » est le séparateur, les crochets « [ » et « ] » ne sont pas obligatoires), exemple: « DS:[DI] ». Avec le 386, les offsets sont passés en 32 bits pour accéder à plus de mémoire (plus de 16 Mo), les offsets avec le préfixe « E » sont apparus (ESI, EDI, ESP, EBP), et comme pours les registres 32 bits, les registres 16 bits, les offsets SI,DI,SP,BP en sont leur partie basse.
    Consultez la Liste des registres.Interruption : Une interruption est comme un petit programme stocké en mémoire qui est appelé plus ou moins régulièrement et qui a une tâche spécifique. Il y en a 256 au maximum, et les premières sont intégrées dans le BIOS (programme lancé au démarrage de l'ordinateur qui gère le matériel : disque dur, accès mémoire...) comme les interruptions de la carte vidéo ou le clavier. Certaines sont appelées à une fréquence constance : l'interruption 1Ch par exemple qui est un compteur qui incrémente (ajoute 1) une valeur 18.6 fois/seconde. D'autres sont appelées uniquement si on a besoin d'elle : l'interruption 09h du clavier est appelée à chaque pression ou relâchement d'une touche.
    Voir la liste complète des interruptions.
  • Pile : La pile pourrait être comparée à une pile d'assiettes, on entasse des assiettes (ouais bon, c'est une image) avec l'instruction "PUSH" puis on les enlêve avec l'instruction "POP". La hauteur de la pile est la valeur du registre [E]BP. On voir ce qu'il y a à l'étage X avec une instruction dans le style « MOV AX,[BP-4] », mais pas l'enlever, car si on enlève une assiette au milieu de la pile, la pile dégringole (= plantage du PC).
  • Flags (drapeaux) : Ceux sont des bits internes au processeur.
    Consultez la Liste des drapeaux.

III. Liste des registres

Il existe plusieurs registres ayant un sens plus ou moins précis (ils peuvent avoir une utilisation différente) :

  • AL/AH/EAX : Registre général, sa valeur change très vite.
  • BL/BH/EBX :Registre général, peut servir d'offset mémoire (exemple : « mov al, byte ptr ds:[bx+10] »).
  • CL/CH/ECX : Sert en général de compteur pour les boucles (exemple : « mov ecx, 5 ; rep movsd » : copie 5 doubles mots).
  • DL/DH/EDX :Registre général, obligatoire pour l'accès aux ports (moyen de communiquer avec toutes les puces de l'ordinateur, par exemple les ports 42h et 43h servent à contrôler le haut-parleur interne, voir IN et OUT).
  • CS : Segment mémoire du code.
  • DS : Segment mémoire des données.
  • ES : Segment mémoire.
  • FS : Autre segment mémoire.
  • GS : Autre segment mémoire.
  • SS : Segment mémoire de la pile (« S » = Stack = Pile).
  • BP : Offset mémoire, très souvent une copie de SP à laquelle on soustrait une valeur pour lire dans la pile (on ne doit pas modifier SP).
  • EDI/DI : Offset mémoire utilisé avec ES (ou FS ou GS si spécifié, exemple : « mov al, byte ptr gs:[10] »).
  • EIP/IP : Offset mémoire du code (inaccessible directement, modifiable indirectement avec l'instruction CALL, JMP, ou J[cas]).
  • ESI/SI : Offset mémoire utilisé avec DS.
  • ESP/S : Offset mémoire de la pile.

IV. Liste des drapeaux

  • AF : Auxilliary Flag = Indicateur de retenue auxilliaire.
  • CF : Carry Flag = Indicateur de retenue.
  • DR : Dirrection Flag = Indicateur de direction de traitement des chaînes de caractères.
  • IF : Interrupt Flag = Indicateur d'exécution des interruptions dites « masquables ».
  • OF : Overflow Flag = Indicateur de débordement.
  • PF : Parity Flag = Indicateur de parité -> PF=0 : Impaire, PF=1 : Paire.
  • SF : Sign Flag = Indicateur de signe -> SF=0 : Positif, SF=1 : Négatif.
  • TF : Single Step Flag = Indicateur de débogage.
  • ZF : Zero Flag = Indique une valeur nulle.

V. Liste des instructions

Les caractères « { » et « } » délimitent un paramètre. Il peut être un registre, une zone mémoire ou une immédiate (une valeur, la plupard du temps c'est un nombre).

Le suffixe « [B/W/D] » terminant une instruction doit être remplacé soit par B, soit par W ou soit par D :

  • « B » signifit que l'instruction est en 8 bits (utilise si nécéssaire le registre AL).
  • « W » signifit que l'instruction est en 16 bits (utilise si nécessaire AX).
  • « D » signifit que l'instruction est en 32 bits (utilise si nécessaire EAX).

AND - CALL - CMP - CMPSx - IN - IRET - JMP - J[cas] - LEA - LDS - LES - LFS - LGS - LSS - LODSx - MOV - MOVSx - MOVZX - MUL - NOT - OUT - PUSH - POP - RET - REP - SHL - SHR - STOSx - SCASx - TEST - XOR

  • AND {destination},{masque} : Applique un "et" à {destination} par {masque}. Tout bit de {destination} est mis à 0 si le bit correspondant de {masque} vaut 0, et est inchangé si le bit correspondant vaut 1 :
    0 AND 0 -> 0
    0 AND 1 -> 0
    1 AND 0 -> 0
    1 AND 1 -> 1

    Equivalant en Turbo Pascal : "{destination} := {destination} and {masque}".
    Equivalant en C : "{destion} &= {masque}".

    Voir aussi NOT, OR et XOR.
  • CALL {adresse} : Appelle une procédure qui est à l'adresse {adresse}. Si une ou plusieurs instructions PUSH précèdent un CALL, ceux sont des paramètres qui sont stockés dans la pile. Dans ce cas la procédure commence par "push [e]bp ; mov [e]bp, [e]sp" et on peut trouver la lecture des paramètres avec des instructions du genre "mov {registre},[ebp-{valeur}]". Il ne faudra surtout pas oublier le "RET [valeur]" à la fin de la procédure (voir plus bas).
  • CMP {a}, {b} : Compare les deux variables {a} et {b}. Toujours suivit d'un saut conditionnel ("J[cas] {offset}", voir sa signification plus bas).
  • CMPS[B/D/W] : Compare l'octet/le mot/le double-mot DS:ESI à ES:EDI.
  • IN {destination},{port} : Lit une valeur 8 bits sur le port {port} (16 bits) et la stocke dans {destination}. Le seul registre autorisé pour {port} est DX. Voir aussi OUT.
  • IRET {valeur} : Quitte l'interruption. Est uniquement utilisé pour les programmes résident comme le pilote de la souris par exemple. Voir aussi RET.
  • JMP {offset} : Va ("J" = Jump : Sauter) à l'adresse {offset}.
  • J[cas] {offset} : Va à l'adresse {offset} si la condition [cas] est exacte. [cas] est une condition relative aux "flags" (drapeaux), elle doit être traduite par « Si le résultat d'une opération logique ... » (je prends comme exemple "CMP {a},{b}") :
non signé | JA | est supérieur (a > b), si CF=ZF=0.
 | JAE ou JNB ou JNC | est supérieur ou égal (a => b), si CF=0.
 | JB ou JC | est inférieur (a <b), si CF=1.
 | JBE | est inférieur ou égal (a <= b), si CF=ZF=1.
signé | JG | est supérieur (a > b), si SF=ZF=0.
 | JGE | est supérieur ou égal (a => b), si SF=OF.
 | JL | est inférieur (a < b), si SF<>OF.
 | JLE | est inférieur ou égal (a <= b), si SF<>OF et ZF=1.
égalité | JE ou JZ | est égal (a = b), si ZF = 1.
 | JNE ou JNZ | est différent (a <> b), si ZF = 0.
  • LEA {destination},{source} : Ecrit l'adresse de {source} dans {destination}. Equivaut à "MOV {destination}, OFFSET {source}".
  • LDS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre DS, son segment, et dans {destination} (16 bits), son offset.
  • LES {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre ES, son segment, et dans {destination} (16 bits), son offset.
  • LFS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre FS, son segment, et dans {destination} (16 bits), son offset.
  • LGS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre GS, son segment, et dans {destination} (16 bits), son offset.
  • LSS {destination},{adresse} : Copie l'adresse {adresse} en 32 bits dans le registre SS, son segment, et dans {destination} (16 bits), son offset.
  • LODS[B/D/W] : Copie l'octet/le mot/le double-mot ES:EDI dans AL/AX/EAX (instruction inverse de STOS[B/W/D]).
  • MOV {dst},{src} : Copie la valeur {src} dans {dst}.
  • MOVS[B/D/W] : Copie l'octet/le mot/le double-mot DS:ESI dans ES:EDI.
  • MOVZX {dst},{src} : Etend à 32 bits le nombre contenu dans {src} (8 bits) et transfert le résultat dans {dst} (16 ou 32 bits).
    Exemple: MOVZX ax,al : Efface la partie haute de AX (AH).
  • MUL {source} : Multiplie la destination explicite par {source}, les deux nombres sont considérés comme non signés. Utiliser "JC {adresse}" pour tester le débordement. La destination est fonction de la taille de source :
    • 8 bits : la destination est AX (le multiplicateur est AL)
    • 16 bits : la destination est DX:AX, c'est à dire AX contient la partie basse et DX la partie haute (le multiplicateur est AX)
    • 32 bits : la destination est EDX:EAX, c'est à dire EAX contient la partie basse et EDX la partie haute (le multiplicateur est EAX)
  • NOT {destination} : Inverse les bits de {destination} :

    NOT 1 -> 0
    NOT 0 -> 1

    Equivalant en Turbo Pascal : "{destination} := not {destination}".
    Equivalant en C : "{destion} = ~{destination}".

    Voir aussi AND, OR et XOR.
  • OR {destination}, {masque} : Applique un "OU logique" à {destination} par {masque}. Tout bit de {destination} est mis à 1 si le bit correspondant de {masque} vaut 1, et laissé inchangé si le bit correspondant vaut 0.

    0 OR 0 -> 0
    0 OR 1 -> 1
    1 OR 0 -> 1
    1 OR 1 -> 1

    Equivalant en Turbo Pascal : "{destination} := {destination} or {masque};".
    Equivalant en C : "{destion} |= {masque}".

    Voir aussi AND, NOT et XOR.
  • OUT {source},{port} : Ecrit la valeur {source} (8 bits) sur le port {port} (16 bits). Le seul registre autorisé pour {port} est DX. Voir aussi IN.
  • PUSH {valeur} : Met une [valeur] dans la pile.
  • POP {registre} : Sort une valeur de la pile et la stocke dans un [registre].
  • REP {instruction}: Répète l'instruction [instuction] ECX fois.
  • RET {valeur} : Quitte la procédure en cours. Si des paramètres ont été envoyés au CALL, [xxxx] est le nombre d'octets envoyés qui sont à sortir de la pile. Voir aussi IRET.
  • SHL {registre},{valeur} : Décalage binaire du {registre} de {valeur} vers la gauche (L = Left), les bits apparaissant à droite sont complétés par des zéros. Exemple : "mov al, 3; shl al,2" donne al = 12, car 3 = 0011 et son décalage 1100. En fait décaler de X revient à multiplier par 2^X (3*2^2 = 3*4 = 3*2*2 = 12).
  • SHR {registre},{valeur} : Décalage binaire du {registre} de {valeur} vers la droite (R = Right), les bits apparaissant à gauche sont complétés par des zéros. Exemple : "mov al, 12; shr al,2" donne al = 3, car 12 = 1100 et son décalage 0011. En fait décaler de X revient à diviser par 2^X (12/(2^2) = 12/4 = 3).
  • STOS[B/D/W] : Copie AL/AX/EAX dans l'octet/le mot/le double-mot ES:EDI (inverse de LODS[B/W/D]).
  • SCAS[B/D/W] : Compare AL/AX/EAX à l'octet/le mot/le double-mot ES:EDI (permet de rechercher une valeur dans une chaine de caractères).
  • TEST {source},{masque} : Teste si les bits {masque} de {source} sont posés ou non, et modifie ZF en conséquence (ZF posé si les bits de {source} sont posés, sinon ZF=0), ce qui sera exploitable avec "JZ" ou "JNZ" par la suite. L'instruction permet de tester un bit particulier de {source}.
    En particulier : TEST {a},{a} = Teste si la variable {a} est à zéro (pose ou non le drapeau ZF).
  • XOR {destination},{masque} : Applique un "ou exclusif" à {destination} par {masque}. Tout bit de {destination} est mis à 1 s'il diffère du bit correspondant de {masque}, et est mis à 0 s'il a la même valeur :
    0 XOR 0 -> 0
    0 XOR 1 -> 1
    1 XOR 0 -> 1
    1 XOR 1 -> 0
    (c'est le même système que pour la multiplication de nombres relatifs : (+2)x(-3) = (-6), et (-3)x(-3)=9 par exemple)

    XOR est utilisé en cryptographique car appliquer deux fois XOR à un même nombre avec le même "masque" redonne le nombre. Exemple :
    24 xor 3 -> 27
    27 xor 3 -> 27

    XOR {a},{a} : Met à la variable {a} à zéro, beaucoup plus rapidement que "MOV {a},0" car XOR est une instruction de base du processeur.

    Equivalant en Turbo Pascal : "{destination} := {destination} xor {masque}".
    Equivalant en C : "{destion} ^= {masque}".

    Voir aussi AND, NOT et OR.

VI. Liste des interruptions

J'ai trouvé des informations par-ci, par-là, donc ne me demandez pas de tout détailler, je n'en sais pas plus.

  • 01h: Lancement d'un programme en mode pas-à-pas (permet de le débogueur).
  • 02h: Interruption non masquable.
  • 03h: Erreur de rupture.
  • 04h: Erreur de dépassement (par calcul, exemple : "mov al, 200 ; add al,140" -> 340 > 255 !!!).
  • 05h "Imprime écran", imprime une copie de l'écran en mode texte.
  • 08h: Horloge tournant à 18.6 clics/seconde.
  • 09h: Lecture du clavier. La touche est codée avec un 'code clavier' (scan code en anglais) traduit en code standart ASCII par l'interruption 16h.
  • 0Bh: Gestion du port COM2.
  • 0Ch: Gestion du port COM1.
  • 10h: Gestion de la carte vidéo.
  • 11h : Liste de configuration (mémoire, nombre de ports COM, co-processeur, ...).
  • 12h: Taille de la mémoire basse (640 Ko maximum).
  • 13h: Gestion des différents disques.
  • 14h: Gestion de l'interface série (ports COM, voir les interruptions 0Bh et 0Ch).
  • 15h: Manette de jeu, cassette et TopView.
  • 16h: Conversion du code de la touche (lu par l'interruption 09h) en code standart ASCII.
  • 17h: Gestion de l'imprimante.
  • 18h: Rom BASIC.
  • 19h: Rountine de chargement du DOS.
  • 1Ah: Gestion de l'heure réelle.
  • 1Bh: Surveille la pression de la combinaison de touche 'CTRL + C'.
  • 1Ch: Chronomètre clic/clic à la vitesse de l'horloge 08h : 18.6 Hz. Sa valeur est stockée à l'emplacement 0040h: 0060h.
  • 1Dh: La table d'initialisation vidéo.
  • 1Eh: La table de paramètre des disquettes.
  • 1Fh: La table des caractères graphiques.
  • 20h: L'interruption DOS : Fin d'un programme au format COM (le format EXE est largement plus répendu aujourd'hui).
  • 21h: L'interruption DOS : Fonctions universelles (disque dur, horloge, ...).
  • 22h: L'interruption DOS : Adresse de fin de processus.
  • 23h: L'interruption DOS : Surveille CTRL + PAUSE (ou CTRL + BREAK).
  • 24h: L'interruption DOS : Erreur fatale d'un vecteur d'interruption.
  • 25h: L'interruption DOS : Lecture directe d'un disque.
  • 26h: L'interruption DOS : Ecriture directe sur un disque.
  • 27h: L'interruption DOS : Programmes résidents.
  • 28h: Fin d'un programme restant résidant en mémoire.
  • 2Fh: Interruption pour plusieurs sous programmes. Gestion du réseau, driver CD-Rom MSCDEX, ...

VII. Exemples concrets

VII-A. Lecture d'une touche au clavier

Principe : l'interruption 16h gère le clavier. Elle possède deux fonctions intéressantes : 00h, lecture d'une touche; et 01h, vérification de la présence d'une touche dans le tampon clavier. Mais si une touche est étendue, par exemple les touches fléchées, ou les touches "page haut", "insertion", etc., la fonction 00h nous reverra un code null (00h) comme code ASCII, puis le code ASCII de la touche étendue. Sachant que le code ASCII est toujours inférieur à 128, on pourra ajouter 128 aux codes étendus pour au final n'avoir à appeler notre future fonction de lecture d'une touche qu'une seule fois.

Solution Turbo Pascal

1.(uses Dos;)
2.const MasqueFlagsCF = 1; { Masque pour isoler le bit CF des flags } (1)
3.
4.function TouchPresse : Boolean;
5.var Regs: Registers; (2)
6.begin with Regs do begin
7. AH := $01; { Fonction 01h: Vérification de la présence d'une touche }
8. Intr ($16,Regs); (3)
9. TouchPresse := (Flags and MasqueFlagsCF = MasqueFlagsCF); (1)
10.end end;
11.
12.function LitTouche : Char;
13.var Regs: Registers;
14.begin with Regs do begin
15. AH := $00; { Fonction 00h : Lecture d'une touche }
16. Intr ($16,Regs);
17. if AL=0 then (4)
18.  LitTouche := Char(AH or 128) (5)
19. else
20.  LitTouche := Char(AL);
21.end end;

Analyse :

  • (1) - Le type Registre (2) nous met à disposition les Flags, met dans un mot d'ensemble (Word). Pour isoler le flag CF (bit #0), on utilisera le truc : "if Flags and MASQUE = MASQUE" où MASQUE est une puissance de 2, la puissance est la position du bit (en partant de zéro). CF étant à la position 0 (bit #0), le MASQUE est donc 1 (2^0).
  • (2) - Pour appeler une interruption, Turbo Pascal nous met à disposition (par l'unité DOS) le type "Registers" qui permet d'accéder de façon virtuelle aux registres. Virtuelle car celà ne modifie par directement les registres, les registres sont tous envoyés comme "paramètre", puis sont lus après l'appelle de "Intr" (3) (appelle d'une l'interruption).
  • (3) - Enfin pour appeler une interruption, on utilise la fonction "Intr" avec comme paramètre le numéro de l'interruption et les registres. Ici on utilise la fonction 00h de l'interruption 16h. Celle-ci retourne dans AL le code ASCII de la touche et AH contient le code étendu de la touche (si AL=0).
  • (4) - Pour savoir si une touche est étendue, on vérifie que AL = #0.
  • (5) - Pour ajouter 128 à la valeur de la touche étendue, on peut également faire "OR 128", celà pose dans tous les cas le bit #7 de AH. Ca ne fonctionne que pour les puissances de 2 (1, 2, 4, 8, 16, 32, 64, 128, 256, ...).

Solution Assembleur

1.function TouchPresse : boolean; assembler; asm
2. mov ah,01h { Fonction 01h = Vérifit qu'une touche soit disponnible }
3. int 16h { Appel l'interruption clavier }
4. mov al,1 { Une touche est présente (TRUE) }
5. jnz @PasVide (1)
6. xor al,al { Pas de touche touche (FALSE) }
7.@PasVide:
8.end;
9.
10.function LitTouche : char; assembler; asm
11.  xor ah,ah { Fonction 00h = Lit une touche du clavier }
12.  int 16h { Appel l'interruption clavier }
13.  or al,al { Est-ce une touche étendue? (AL=0) } (2)
14.  jz @Etendue { Ouais -> AL = code étendu (AH) + 128 } (2)
15.  ret { Touche standart -> on se tire }
16.
17.@Etendue:
18.  mov al,ah { AL = Code ASCII étendu }
19.  or al,128 { Ajoute 128 à celui-ci pour le distinguer }
20.end;

Analyse :

  • (1) - La fonction 01h de l'interruption 16h renvoi la présence d'une touche par le flag ZF. Pour le tester on utilise JNZ : ZF=0, ne fait rien; ZF=1 : Saute !
  • (2) - La fonction 00h de l'interruption 16h renvoi le code ASCII de la touche dans le registre AL. Si celui-ci vaut 0, alors la touche est étendue, et le code est stocké dans AH. Pour savoir si AL=0, on peut faire "cmp al,0; jz @Saut", mais il est plus rapide de faire le test par "or". Celui-ci modifie le flag ZF : Si AL=0, ZF=1 (ZF = Zero Flag !); si AL<>0, ZF=0.

VII-B. Effacement de l'écran dans le mode VGA (320x200 pixels en 256 couleurs)

Dans le mode VGA, la mémoire vidéo est placée à l'adresse $A000:$0000, et prend 320*200=64 000 octets. Le but du jeu est de remplir ces 64 000 octets avec une couleur précise, c'est à dire un valeur codée sur un octet.

Solution en Turbo Pascal

1.           fillchar (Mem[$A000:0],64000,Couleur); { Couleur étant un "Byte", un octet }

Celà fonctionne très bien, mais Turbo Pascal étant développé pour fonctionner avec les 286, processeur en 16 bits, fillchar fonctionnera donc en 16 bits (voir en 8 bits, je ne sais pas trop). On va donc l'optimiser en passant écrivant dans la mémoire avec les instructions 32 bits.

Solution Assembleur (intégrée dans un programme Turbo Pascal, première version, dite "éducative")

1.Procedure EffaceEcran (Coul: byte); begin
2.asm (1)
3.mov ax,0A000h (2)
4.mov es,ax { ES = A000h }
5.mov di,0 { Maintenant ES:DI pointe sur l'adresse A000:0000 }
6.mov al,[coul] { On lit la couleur passée en paramètre } (3)
7.mov cx,64000 { 64 000 octets à remplir}
8.rep stosb { Ecrit CX fois l'octet AL à l'adresse ES:DI }
9.end; (1)
10.end;

Analyse :

  • (1) - En turbo Pascal, on peut intégrer de l'assembleur n'importe où en tapant "asm { les instructions } end;".
  • (2) - Les nombres hexadécimaux débutants par des lettres peuvent être confondus avec des noms de variables, il faut alors rajouter un zéro en préfixe.
  • (3) - Pour lire les paramètres, rien de plus simple : taper son nom (les crochets ne sont pas obligatoires).

Solution Assembleur (version optimisée 32 bits)

1.
2.Procedure EffaceEcran (Coul: byte); Assembler; asm (1)
3.mov ax,0A000h
4.mov es,ax
5.{ xor edi, edi } (2) db 66h; xor di,di (3) { ES:DI pointe sur l'adresse A000:0000 }
6.mov al,[coul] { On lit la couleur passée en paramètre }
7.mov ah, al { Copie AL dans AH, donc AX = deux fois la couleur } (4)
8.mov bx, ax { Sauve AX dans le registre BX } (4)
9.{ shl eax,16 } db 66h; shl ax,16 { Déplace les deux octets de poids faible de EAX dans les octets de poids fort } (4)
10.mov ax, bx { Relit AX du registre BX } (4)
11.mov cx,64000/4 { 16 000 double-mots à remplir } (5)
12.{rep stosd } db 66h; rep stosw { Ecrit ECX fois le double-mot EAX à l'adresse ES:EDI } (6)
13.end; (1)

Analyse :

  • (1) - Encore mieux, en turbo Pascal on peut déclarer une procédure qui n'utilise que l'assembleur, ce qui évite d'avoir à taper "begin" et "end;" inutiles.
  • (2) - Pour mettre un registre à zéro, la méthode la plus rapide est "xor reg,reg", car XOR est une fonction de base du processeur.
  • (3) - Turbo Pascal 7 ne connaissant pas les instructions 32 bits, on peut toujours les écrire en langage machine. Pour avoir le code machine d'une instruction, on peut compiler un programme en assembleur ne contenant que cette instruction en demandant au compileur de créer un listing (avec le paramètre "/Z" pour TASM 1.0 et 2.0), c'est à dire un fichier contenant le code assembleur décomposée, interprété et traduit en code machine. Sinon, il existe aussi des programmes effectuant la convertion, mais je n'en connais pas directement. Pour revenir à notre "xor edi, edi", l'équivalant en code machine est "66 33 FF", or "xor di,di" est équivaut à "33 FF", il suffit donc de rajouter le code "66h" en préfixe, préfixe de nombreuses instructions 32 bits en fait.
  • (4) - En 32 bits, l'instruction "STOSD" écrira EAX et non AL, il faut donc répéter "Coul" quatre fois dans EAX. On commence par l'écrire dans AL, puis le copier dans AH, et enfin un "truc" pour le copier dans EAX. Le truc est en fait de décaler les octets de EAX de 16 bits vers la gauche à l'aide de l'instruction SHL, puis recopier l'ancienne valeur de AX dans AX (qui est la partie basse de EAX pour souvenir !). Remarque : l'instruction "SHL" nécessite de compiler en mode 286/287, on peut rajouter "{$G+}" au début du programme pour ce fait.
  • (5) - STOSD écrit des double-mots (4 octets), or nous voulons remplir 64 000 octets. Il nous faudra donc écrire 64 000/4 fois EAX. Remarque : pour STOSW, on divisera par 2 tout simplement :-)
  • (6) - Une fois de plus, on peut étendre une instruction 16 bits à son équivalant en 32 bits en rajoutant le préfixe "66h". En réalité "REP STOSW" est "F3 AB" (F3 = "REP"), et "REP STOSD" est "F3 66 AB". Mais le processeur est tolérant, et le code est d'autant plus clair avec "db 66h; stosw", alors pourquoi s'en priver.


Apprendre l'assembleur

I. Introduction

« Désormais, pour apprendre le français, il faudra SAVOIR le français ! » disait Coluche. De même, ce tutoriel s'adresse à tous ceux qui connaissent déjà les rudiments d'un langage évolué tel que le BASIC, le FORTRAN, le C ou le PASCAL et qui souhaitent apprendre l'assembleur. Une connaissance même sommaire d'un de ces langages suffit ! Aucune connaissance en programmation système n'est requise : le but de ce cours est justement d'en introduire les fondements.

Contrairement aux langages évolués, l'assembleur, ou « langage d'assemblage » est constitué d'instructions directement compréhensibles par le microprocesseur : c'est ce qu'on appelle un langage de bas niveau. Il est donc intimement lié au fonctionnement de la machine. C'est pourquoi il est relativement difficile à assimiler, en tout cas beaucoup plus que les langages de haut niveau.

Cela explique également pourquoi il existe au moins autant de langages d'assemblage que de modèles de microprocesseurs.

Avant d'apprendre l'assembleur INTEL 80x86, il est donc primordial de s'intéresser à quelques notions de base concernant par exemple la mémoire ou le microprocesseur. C'est là en effet que se trouvent les principales difficultés pour le débutant. Ne soyez pas rebuté par l'abstraction des concepts présentés dans les premiers paragraphes : il est normal que durant la lecture, beaucoup de choses ne soient pas claires dans votre esprit. Tout vous semblera beaucoup plus simple quand nous passerons à la pratique dans le langage assembleur.

Le microprocesseur peut fonctionner sous deux modes : le mode réel et le mode protégé. Le mode protégé permet d'accéder à 232 octets de mémoire vive, alors que le mode réel ne peut en adresser que 220 = 1 Mo. Nous ne traiterons dans ce cours que le mode réel. C'est celui qu'utilisent la plupart des programmes DOS.

Convention : toutes les adresses sont écrites en notation hexadécimale. Les autres nombres seront la plupart du temps représentés en base décimale. Dans le cas contraire, nous ajouterons la lettre ‘h' après les chiffres.

II. PREMIÈRE PARTIE

II-A. L'ARITHMÉTIQUE SIGNÉE

On appelle arithmétique non signée l'arithmétique dans laquelle tous les entiers sont positifs. En arithmétique signée au contraire, les nombres peuvent être soit positifs, soit négatifs. Un nombre signé n'est donc pas forcément négatif.

Les données informatiques se présentent sous la forme d'une succession de chiffres binaires, les bits. Nous supposerons que les systèmes de numération binaire et hexadécimal vous sont déjà familiers.

Il est en revanche fort possible que vous ne connaissiez pas la façon dont un ordinateur représente les nombres négatifs. Il existe deux conventions : la notation en signe et valeur absolue et la notation en complément à 2.

La première est extrêmement simple : le premier bit représente le signe, et les autres bits la valeur absolue du nombre. Le bit de signe vaut 1 si le nombre est strictement négatif, 0 sinon.

Par exemple, le nombre (-14) codé sur 8 bits s'écrit ainsi :

10001110

Cette convention n'est quasiment jamais utilisée en informatique. On lui préfère la représentation en complément à 2 dont le principe est le suivant :

  • les nombres positifs sont codés de la même façon qu'en convention « signe et valeur absolue » ;
  • les nombres négatifs sont obtenus en inversant tous les bits, puis en ajoutant 1.

Exemple : le nombre 14 codé sur 8 bits est représenté ainsi :

00001110

et (-14) ainsi :

  • inversion des bits : 11110001 ;
  • ajout d'une unité : 11110010 ;
  • résultat : 11110010.

Remarque : le résultat intermédiaire, 11110001, est appelé « complément à 1 ».

Vous allez immédiatement comprendre l'avantage de cette représentation. Faisons la somme de 14 et de (-14), de la même façon que s'il s'agissait d'entiers positifs :

00001110 + 11110010 = 100000000

Le résultat étant codé sur 8 bits, le 1 situé à gauche n'est pas pris en compte. On obtient donc 14 + (-14) = 0.

L'intérêt évident est que la différence de deux nombres peut se calculer avec le même algorithme que leur somme. Il suffit de transformer au préalable le nombre retranché en son opposé. Cette conversion est très simple et très rapide.

Au contraire, en représentation « signe et valeur absolue », on aurait eu besoin de nombreux algorithmes, car plusieurs cas se présentent.

Remarque : la représentation en complément à 2 revient en fait à écrire (-1) comme ceci :

11111111

(-2) comme cela :

11111110

(-3) comme cela :

11111101

etc.

Il y a ainsi une symétrie entre les nombres positifs et les nombres négatifs. Il en résulte que le bit le plus à gauche représente le signe, de la même façon qu'en notation « signe et valeur absolue ». Avant tout calcul, nous pouvons donc affirmer que le nombre 10010101 est négatif.

II-B. >LA MÉMOIRE VIVE

II-B-1. La segmentation de la mémoire

Votre PC est conçu pour gérer 1 Mo (soit 220 octets) de mémoire vive en mode réel. Il faut donc 20 bits au minimum pour adresser toute la mémoire. Or en mode réel les bus d'adresses n'ont que 16 bits. Ils permettent donc d'adresser 216 = 65536 octets = 64 Ko, ce qui est insuffisant !

Afin de pallier ce manque, on utilise deux nombres pour adresser un octet quelconque de la RAM. Le premier est appelé adresse de segment, le second adresse d'offset. Ils seront stockés séparément.

La mémoire est découpée en segments de 64 Ko chacun. Un segment est donc en quelque sorte un gros bloc de mémoire auquel on peut accéder grâce à une adresse de segment qui désigne son numéro. Par exemple, le premier segment est le segment 0000 (en hexa), le deuxième est le 0001, le quarante-deuxième est le 0029, etc. Chaque numéro est codé sur 16 bits, c'est-à-dire 4 chiffres hexa.

Pour accéder à un octet particulier dans un segment, il suffit de compter le décalage de cet octet par rapport au début du segment. Ce décalage est obligatoirement inférieur ou égal à 65535 : il tient bien sur 16 bits lui aussi. On appelle ce décalage « offset ».

L'adresse d'un octet se note XXXX:YYYY où XXXX est l'adresse de segment et YYYY est l'offset (tous deux en notation hexadécimale, bien sûr).

Par exemple, le dix-septième octet de la RAM (le numéro 16) est situé à l'adresse 0000:0010. De même, l'octet 0000:0100 est l'octet numéro 256. Nous en arrivons à la petite subtilité qu'il convient de bien saisir, sous peine de ne rien comprendre à certains programmes en assembleur.

On pourrait penser que l'octet qui se trouve à l'adresse 0001:0000 est le numéro 65536. Il n'en est rien. C'est l'octet numéro 16.

Eh oui ! Les segments ne sont pas situés gentiment les uns à la suite des autres. Ils sont fort mal élevés et n'attendent pas que les segments qui les précèdent soient terminés avant de commencer ! Ils se marchent donc sur les pieds.

Autrement dit, le deuxième segment ne démarre pas à l'octet 65536 comme il devrait le faire s'il était bien sage, mais à l'octet 16 ! Le troisième démarre à l'octet 32 et ainsi de suite…

La notion de segment n'est pas tant physique que mathématique : elle sert à se repérer dans la RAM.

La conséquence immédiate de tout cela est qu'un octet n'a pas une adresse unique. Par exemple, l'octet numéro 66 peut être adressé par 0000:0042, mais aussi par 0001:0032, par 0002:0022, par 0003:0012 ou encore par 0004:0002. Toutes ces adresses sont équivalentes.

Voilà pour la subtilité. Si vous avez compris, vous devriez être capable de trouver facilement comment on calcule l'adresse effective d'un octet, c'est-à-dire sa position absolue dans la RAM.

Allez, un petit effort !

Voici la solution : si l'adresse de l'octet est A17C:022E, alors son adresse effective est A17C x 16 + 022E, soit A17C0 + 022E = A19EE. On a multiplié par 16, car le segment A17C débute à l'octet A17C x 16, puis on a simplement ajouté le décalage.

Au final, on a bien une adresse sur 20 bits puisqu'on obtient 5 chiffres hexa. Chaque petit bloc de 16 octets s'appelle un paragraphe.

Image non disponible

Il est souvent plus simple de considérer qu'un segment est un bloc de taille quelconque qui débute à une adresse effective multiple de 16 et qui permet, à l'aide de son adresse de segment et d'un offset, d'adresser le bloc entier (64 Ko au maximum). Cette définition est à notre avis celle qui a le plus de sens, et nous l'utiliserons tout au long de ce cours.

II-B-2. Structure d'un programme en mémoire

Lorsque l'utilisateur exécute un programme, celui-ci est d'abord chargé en mémoire par le système. Le DOS distingue deux modèles de programmes exécutables : les fichiers COM et les fichiers EXE.

La différence fondamentale est que les programmes COM ne peuvent pas utiliser plus d'un segment dans la mémoire. Leur taille est ainsi limitée à 64 Ko. Les programmes EXE ne sont quant à eux limités que par la mémoire disponible dans l'ordinateur.

II-B-3. Les fichiers COM

Lorsqu'il charge un fichier COM, le DOS lui alloue toute la mémoire disponible. Si celle-ci est insuffisante, il le signale à l'utilisateur par un message et annule toute la procédure d'exécution. Dans le cas contraire, il crée le PSP du programme au début du bloc de mémoire réservé, et copie le programme à charger à la suite.

Mais qu'est-ce donc qu'un PSP ?

Pour simplifier, le PSP (« Program Segment Prefix ») est une zone de 256 (= 100h) octets qui contient des informations diverses au sujet du programme. C'est dans le PSP que se trouve la ligne de commande tapée par l'utilisateur. Par exemple, le PSP d'un programme appelé MONPROG, exécuté avec la commande « MONPROG monfic.txt /S /H », contiendra la chaîne de caractères suivante : « monfic.txt /S /H ». Le programmeur a ainsi la possibilité d'accéder aux paramètres.

Voici à titre indicatif la structure simplifiée du PSP (ne vous souciez pas de ce que vous ne comprenez pas : pour l'instant, seules les deux dernières lignes nous intéressent vraiment) :

Offset | Description | Taille (octets)
00 h | Appel de l'int 20h | 2
02 h | Adresse du 1er segment qui se trouve au-delà du prog. | 2
04 h | Réservé | 1
05 h | Far call de l'int 21h (inutilisé) | 5
0A h | Vecteur de l'int 22h | 4
0E h | Vecteur de l'int 23h | 4
12 h | Vecteur de l'int 24h | 4
16 h | Réservé | 22
2C h | Segment du bloc d'environnement | 2
2E h | Réservé | 82
80 h | Nombre de caractères dans la ligne de commande sans compter le code ASCII 13 (retour chariot) | 1
81 h | Ligne de commande (à partir du caractère espace qui suit le nom du programme) + code ASCII 13 | 127

À présent que nous connaissons l'existence du PSP, il nous faut revenir sur un point important. Comme nous l'avons dit, un programme COM ne peut comporter qu'un seul segment, bien que le DOS lui réserve la totalité de la mémoire disponible. Ceci a deux conséquences. La première est que les adresses de segment sont inutiles dans le programme : les offsets seuls permettent d'adresser n'importe quel octet du segment. La seconde est que le PSP fait partie de ce segment, ce qui limite à 64 Ko - 256 octets la taille maximale d'un fichier COM. Cela implique également que le programme lui-même débute à l'offset 100h et non à l'offset 0h.

II-B-4. Les fichiers EXE

« Pour un fichier EXE, tout est toujours un peu plus compliqué. »
(Devise du programmeur débutant en assembleur)

Bien qu'il soit possible de n'utiliser qu'un seul segment à tout faire, la plupart des programmes EXE ont un segment réservé au code (c'est ainsi qu'on appelle les instructions du langage machine), un ou deux autres aux données, et un dernier à la pile.

La pile est une mémoire très spéciale qui sert comme son nom l'indique à empiler des données de 16 bits de façon temporaire. On peut ensuite retrouver ces données en les dépilant. Le « dépilage » se fait toujours dans l'ordre inverse de l'empilage.

Le PSP a lui aussi son propre segment. Le programme commence donc à l'offset 0h du segment de code et non à l'offset 100h.

Afin que le programme puisse être chargé et exécuté correctement, il faut que le système sache où commence et où s'arrête chacun de ces segments. À cet effet, les compilateurs créent un en-tête (ou « header ») au début de chaque fichier EXE. Ce header ne sera pas copié en mémoire. Son rôle est simplement d'indiquer au DOS (lors du chargement) la position relative de chaque segment dans le fichier.

II-B-5. Les cycles de lecture-écriture

Le code et les variables d'un programme se trouvent dans la mémoire vive. Pour accéder à une donnée en mémoire, le processeur place son adresse dans un bus d'adresse. Un cycle de lecture se met alors en place. Il consiste à retourner la donnée lue au processeur via le bus de données.

Pour l'écriture, l'adresse de la destination est transmise dans le bus d'adresse et la donnée à écrire est placée dans le bus de données.

Ouf !

Nous aurons l'occasion de revenir sur la mémoire vive dans les autres chapitres pour apporter quelques précisions. Vous devriez mieux comprendre toutes ces notions en lisant la suite, car jusqu'ici nous n'avons pas encore parlé de la façon dont l'ordinateur se sert des adresses pour accéder à des données ou pour exécuter du code. C'est là qu'intervient le microprocesseur…

II-C. LE MICROPROCESSEUR - LES REGISTRES

II-C-1. Généralités sur les registres

Le microprocesseur est le cœur de l'ordinateur. C'est lui qui est chargé de reconnaître les instructions et de les exécuter (ou de les faire exécuter). Chaque instruction se présente sous la forme d'une suite de bits qu'on représente en notation hexadécimale (exemple : B44C, ou 1011010001001100 en binaire). Une instruction se compose d'un code opérateur (le plus souvent appelé « opcode ») qui désigne l'action à effectuer (B4 dans notre exemple) et d'un « champ d'opérandes » sur lesquels porte l'opération (ici l'opérande est 4C).

Pour travailler, le microprocesseur utilise de petites zones où il peut stocker des données. Ces zones portent le nom de registres et sont très rapides d'accès puisqu'elles sont implantées dans le microprocesseur lui-même et non dans la mémoire vive.

Les registres des processeurs INTEL se classent en quatre catégories :

  • les registres généraux (16 bits) ;
  • les registres de segment (16 bits) ;
  • les registres d'offset (16 bits) ;
  • le registre des indicateurs (16 bits).

II-C-2. Les registres généraux

Ils ne sont pas réservés à un usage très précis, aussi les utilise-t-on pour manipuler des données diverses. Ce sont en quelque sorte des registres à tout faire. Chacun de ces quatre registres peut servir pour la plupart des opérations, mais ils ont tous une fonction principale qui les caractérise.

Nom | Fonction privilégiée
AX | Accumulateur
BX | Base
CX | Compteur
DX | Données

Le registre AX sert souvent de registre d'entrée-sortie : on lui donne des paramètres avant d'appeler une fonction ou une procédure. Il est également utilisé pour de nombreuses opérations arithmétiques, telles que la multiplication ou la division de nombres entiers. Il est appelé « accumulateur ».

Exemples d'utilisation :

  • l'instruction “MOV AX, 1982” place l'entier 1982 dans AX ;
  • “ADD AX, 1983” ajoute à AX le nombre 1983 et place le résultat dans AX.

Le registre BX peut servir de base. Nous verrons plus tard ce que ce terme signifie.

Le registre CX est utilisé comme compteur dans les boucles. Par exemple, pour répéter 15 fois une instruction en assembleur, on peut mettre la valeur 15 dans CX, écrire l'instruction précédée d'une étiquette qui représente son adresse en mémoire, puis faire un LOOP à cette adresse. Lorsqu'il reconnaît l'instruction LOOP, le processeur « sait » que le nombre d'itérations à exécuter se trouve dans CX. Il se contente alors de décrémenter CX, de vérifier que CX est différent de 0 puis de faire un saut (« jump ») à l'étiquette mentionnée. Si CX vaut 0, le processeur ne fait pas de saut et passe à l'instruction suivante.

Exemple :

Image non disponible

Le registre DX contient souvent l'adresse d'un tampon de données lorsqu'on appelle une fonction du DOS. Par exemple, pour écrire une chaîne de caractères à l'écran, il faut placer l'offset de cette chaîne dans DX avant d'appeler la fonction appropriée.

Chacun de ces quatre registres comporte 16 bits. On peut donc y stocker des nombres allant de 0 à 65535 en arithmétique non signée, et des nombres allant de -32768 à 32767 en arithmétique signée. Les 8 bits de poids fort d'un registre sont appelés « partie haute » et les 8 autres « partie basse ». Chaque registre est en fait constitué de deux « sous-registres » de 8 bits. La partie haute de AX s'appelle AH, sa partie basse AL. Il en va de même pour les trois autres.

En arithmétique non signée, il en découle la formule suivante :

AX = AH x 256 + AL

ainsi que leurs homologues :

BX = BH x 256 + BL

CX = CH x 256 + CL

DX = DH x 256 + DL

Il va de soi qu'en modifiant AL ou AH, on modifie également AX.

Pour stocker un nombre de 32 bits, on peut utiliser des paires de registres. Par exemple, DX:AX signifie DX x 65535 + AX en arithmétique non signée.

Cette notation (DX:AX) n'est pas reconnue par l'assembleur (ni par la machine). Ne confondez pas cela avec les adresses de segment et d'offset.

Sur un PC relativement récent, AX, BX, CX, DX ne sont en fait que les parties basses de registres de 32 bits nommés EAX, EBX, ECX, EDX (« E » pour « Extended »). On a donc un moyen plus pratique de stocker les grands nombres. En fait, chaque registre (pas seulement les registres généraux) peut contenir 32 bits.

II-C-3. Les registres de segment

Ils sont au nombre de quatre :

Nom | Nom complet | Traduction
CS | Code segment | Segment de code
DS | Data segment | Segment de données
ES | Extra segment | Extra segment
SS | Stack segment | Segment de pile

Contrairement aux registres généraux, ces registres ne peuvent servir pour les opérations courantes : ils ont un rôle très précis. On ne peut d'ailleurs pas les utiliser aussi facilement que AX ou BX, et une petite modification de l'un d'eux peut suffire à « planter » le système. Eh oui ! L'assembleur, ce n'est pas Turbo Pascal ! Il n'y a aucune barrière de protection, si bien qu'une petite erreur peut planter le DOS. Mais rassurez-vous : tout se répare très bien en redémarrant l'ordinateur…

Dans le registre CS est stockée l'adresse de segment de la prochaine instruction à exécuter. La raison pour laquelle il ne faut surtout pas changer sa valeur directement est évidente. De toute façon, vous ne le pouvez pas. Le seul moyen viable de le faire est d'utiliser des instructions telles que des sauts (JMP) ou des appels (CALL) vers un autre segment. CS sera alors automatiquement actualisé par le processeur en fonction de l'adresse d'arrivée.

Le registre DS est quant à lui destiné à contenir l'adresse du segment des données du programme en cours. On peut le faire varier à condition de savoir exactement pourquoi on le fait. Par exemple, on peut avoir deux segments de données dans son programme et vouloir accéder au deuxième. Il faudra alors faire pointer DS vers ce segment.

ES est un registre qui sert à adresser le segment de son choix. On peut le changer aux mêmes conditions que DS. Par exemple, si on veut copier des données d'un segment vers un autre, on pourra faire pointer DS vers le premier et ES vers le second.

Le registre SS adresse le segment de pile. Il est rare qu'on doive y toucher car le programme n'a qu'une seule pile.

Intéressons-nous à présent aux valeurs que le DOS donne à ces registres lors du chargement en mémoire d'un fichier exécutable ! Elles diffèrent selon que le fichier est un programme COM ou EXE. Pour écrire un programme en assembleur, il est nécessaire de connaître ce tableau par cœur :

Registre | Valeur avant l'exécution
 | Fichier COM | Fichier EXE
CS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de code
DS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
ES | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse de segment du PSP
SS | Adresse de l'unique segment, c'est-à-dire adresse de segment du PSP | Adresse du segment de pile

Le schéma suivant montre mieux la situation :

Image non disponible

Dans un fichier EXE, le header indique au DOS les adresses initiales de chaque segment par rapport au début du programme (puisque le compilateur n'a aucun moyen de connaître l'adresse à laquelle le programme sera chargé). Lors du chargement, le DOS ajoutera à ces valeurs l'adresse d'implantation pour obtenir ainsi les véritables adresses de segment.
Dans le cas d'un fichier COM, tout est plus simple. Le programme ne comporte qu'un seul segment, donc il suffit tout bêtement au DOS de charger CS, DS, ES et SS avec l'adresse d'implantation.

Remarque : Pourquoi DS et ES pointent-ils vers le PSP dans le cas d'un fichier EXE ? Première raison : pour que le programmeur puisse accéder au PSP ! Deuxième raison : parce qu'un programme EXE peut comporter un nombre quelconque de segments de données. C'est donc au programmeur d'initialiser ces registres, s'il veut accéder à ses données.

II-C-4. Les registres d'offset

Les voici :

Nom | Nom complet | Traduction
IP | Instruction pointer | Pointeur d'instruction
SP | Stack pointer | Pointeur de pile
SI | Source index | Index de source
DI | Destination index | Index de destination
BP | Base pointer | Pointeur de base

Le registre IP désigne l'offset de la prochaine instruction à exécuter, par rapport au segment adressé par CS. La combinaison de ces deux registres (i.e. CS:IP) suffit donc à connaître l'adresse absolue de cette instruction. Le processeur peut alors aller la chercher en mémoire et l'exécuter. De plus, il actualise IP en l'incrémentant de la taille de l'instruction en octets. Tout comme CS, il est impossible de modifier IP directement.

Le registre SP désigne le sommet de la pile. Il faut bien comprendre le fonctionnement de la pile, aussi allons-nous insister sur ce point. La pile ne peut stocker que des mots. On appelle un mot (« word » en anglais) un nombre codé sur deux octets (soit 16 bits). Prenons un exemple simple : un programme COM. Le segment de pile (adressé par SS) et le segment de code ne font qu'un. Avant l'exécution, SP vaut FFFE. C'est l'offset de la dernière donnée de 16 bits empilée, par rapport à SS bien sûr. Pourquoi FFFE ? Tout simplement parce que la pile se remplit à l'envers, c'est-à-dire en partant de la fin du segment et en remontant vers le début. Le premier mot empilé se trouve à l'offset FFFE. Il tient sur deux octets : l'octet FFFE et l'octet FFFF. Mais comment se fait-il qu'un mot soit déjà empilé avant le début du programme ? Ce mot est un zéro que le DOS place sur la pile avant l'exécution de tout programme COM. Nous en verrons la raison plus tard.

À présent, que se passe-t-il si à un instant quelconque une instruction ordonne au processeur d'empiler un mot ? Eh bien le stack pointer sera décrémenté de 2 et le mot sera copié à l'endroit pointé par SP. Rappelez-vous que la pile se remplit à l'envers ! C'est pour cette raison que SP est décrémenté à chaque empilage et non pas incrémenté.

Un petit exemple pour rendre les choses plus concrètes :

PUSH AX

L'effet de cette instruction est d'empiler le mot contenu dans le registre AX. Autrement dit, SP est automatiquement décrémenté de 2, puis AX est copié à l'adresse SS:SP.

Lors du dépilage, le mot situé au sommet de la pile, c'est-à-dire le mot adressé par SS:SP, est transféré dans un registre quelconque choisi par le programmeur, après quoi le stack pointer est incrémenté de 2.

Exemple :

POP BX

Cette fois, on retire le dernier mot empilé pour le placer dans le registre BX. Évidemment, SP sera incrémenté de 2 aussitôt après.

La pile est extrêmement utile lorsqu'il s'agit de stocker provisoirement le contenu d'un registre qui doit être modifié.

Exemple :

Image non disponible

Il est important de comprendre qu'on ne peut dépiler que le mot qui se trouve au sommet de la pile. Le premier mot empilé est le dernier qui sera dépilé. La pile doit être manipulée avec une extrême précaution. Un dépilage injustifié fait planter la machine presque systématiquement.

Les trois derniers registres sont beaucoup moins liés au fonctionnement interne du processeur. Ils sont mis à la disposition du programmeur qui peut les modifier à sa guise et les utiliser comme des registres généraux. Comme ces derniers cependant, ils ont une fonction qui leur est propre : servir d'index (SI et DI) ou de base (BP). Nous allons expliciter ces deux termes.

Dans la mémoire, les octets se suivent et forment parfois des chaînes de caractères. Pour utiliser une chaîne, le programmeur doit pouvoir accéder facilement à tous ses octets, l'un après l'autre. Or pour effectuer une opération quelconque sur un octet, il faut connaître son adresse. Cette adresse doit en général être une constante évaluable par le compilateur. Pourquoi une constante ? Parce que l'adresse est un opérande comme les autres, elle se trouve immédiatement après l'opcode et doit donc avoir une valeur numérique fixe !

Prenons un exemple :

MOV AH, [MonOctet]

Pas de panique ! Cette instruction en assembleur signifie « Mettre dans AH la valeur de l'octet adressé par le label MonOctet ! ». À la compilation, « MonOctet » sera remplacé par la valeur numérique qu'il représente et on obtiendra alors une instruction en langage machine telle que :

8A260601

8A26 est l'opcode (hexa) de l'instruction “MOV AH, [constante quelconque]”, et 0601 est l'offset de « MonOctet ».

Il serait pourtant fastidieux, dans le cas d'une chaîne de 112 caractères, de traiter les octets avec 112 instructions dans lesquelles seule l'adresse changerait. Il faudrait pouvoir faire une boucle sur l'adresse, mais alors celle-ci ne serait plus une constante, d'où le problème.

Pour les constructeurs du microprocesseur, la seule solution était de créer de nouveaux opcodes pour chaque opération portant sur un octet en mémoire. Ces opcodes spéciaux feraient la même action que ceux dont ils seraient dérivés, mais l'adresse passée en paramètre serait alors considérée comme un décalage par rapport à un registre spécial. Il suffirait donc de faire varier ce registre, et le processeur y ajouterait automatiquement la valeur de l'opérande pour obtenir l'adresse réelle ! C'est à cela que servent SI, DI et BP.

Par exemple :

MOV AH, [MonOctet + DI]

sera codé :

8AA50601

8AA5 est l'opcode pour l'instruction “MOV AH, [constante quelconque + DI]”.

Remarque : les registres SI et BP auraient tout aussi bien pu être employés, mais pas les registres généraux, SAUF BX. En effet, BX peut jouer exactement le même rôle que BP. N'oubliez pas que BX est appelé registre de « base », et que BP signifie « Base Pointer. » Nous verrons la différence entre une base et un index lorsque nous commencerons l'assembleur.

II-C-5. Le registre des indicateurs

Un programme doit pouvoir faire des choix en fonction des données dont il dispose. Pour cela, il lui faut par exemple comparer des nombres, examiner leur signe, découvrir si une erreur a été constatée, etc.

Il existe à cet effet de petits indicateurs, les flags qui sont des bits spéciaux ayant une signification très précise. De manière générale, les flags fournissent des informations sur les résultats des opérations précédentes.

Ils sont tous regroupés dans un registre : le registre des indicateurs. Comprenez bien que chaque bit a un rôle qui lui est propre et que la valeur globale du registre ne signifie rien. Le programmeur peut lire chacun de ces flags et parfois modifier leur valeur directement.

En mode réel, certains flags ne sont pas accessibles. Nous n'en parlerons pas. Nous ne commenterons que les flags couramment utilisés.

Nous verrons quelle utilisation on peut faire de ces indicateurs dans la troisième partie de ce tutoriel.

  • CF (« Carry Flag ») est l'indicateur de retenue. Il est positionné à 1 si et seulement si l'opération précédemment effectuée a produit une retenue. De nombreuses fonctions du DOS l'utilisent comme indicateur d'erreur : CF passe alors à 1 en cas de problème.
  • PF (« Parity Flag ») renseigne sur la parité du résultat. Il vaut 1 si ce dernier est pair.
  • AF (« Auxiliary Carry Flag ») est peu utilisé.
  • ZF (« Zero Flag ») passe à 1 si le résultat d'une opération est égal à zéro.
  • SF (« Sign Flag ») passe à 1 si le résultat d'une opération sur des nombres signés est négatif.
  • TF (« Trap Flag ») est utilisé pour le « débogage » d'un programme. S'il vaut 1, une routine spéciale du débogueur est appelée après l'exécution de chaque instruction par le processeur.
  • IF (« Interrupt Flag ») sert à empêcher les appels d'interruptions lorsqu'il est positionné à 1. Cependant, toutes les interruptions ne sont pas « masquables ».
  • DF (« Direction Flag ») est utilisé pour les opérations sur les chaînes de caractères. S'il vaut 1, celles-ci seront parcourues dans le sens des adresses décroissantes, sinon les adresses seront croissantes.
  • OF (« Overflow Flag ») indique qu'un débordement s'est produit, c'est-à-dire que la capacité de stockage a été dépassée. Il est utile en arithmétique signée. Avec des nombres non signés, il faut utiliser ZF et SF.

Remarque : Les notations CF, PF, AF, etc. ne sont pas reconnues par l'assembleur. Pour utiliser les flags, il existe des instructions spécifiques que nous décrirons plus tard.

II-D. LES INTERRUPTIONS

II-D-1. Introduction

Le microprocesseur ne peut exécuter qu'une seule instruction à la fois. Pour connaître son adresse, il utilise le couple de registres CS:IP dont la valeur est incrémentée automatiquement. Par conséquent, le code du programme courant est exécuté de manière linéaire.

Imaginons cependant qu'un événement extérieur demande l'attention de l'ordinateur, par exemple la pression d'une touche du clavier. La machine doit pouvoir réagir immédiatement, sans attendre que le programme en cours d'exécution se termine. Pour cela, elle interrompt ce dernier pendant un bref instant, le temps de traiter l'événement survenu puis rend le contrôle au programme interrompu.

Une interruption n'est rien d'autre que l'appel d'une routine spéciale présente en mémoire appelée ISR (« Interrupt Service Routine »).

Les interruptions se divisent en trois catégories :

  • les interruptions électroniques, par exemple : le clavier ;
  • les interruptions du BIOS, par exemple : l'accès aux disques ;
  • les interruptions du DOS, par exemple : l'accès aux systèmes de fichiers.

Comment sont-elles déclenchées ?

Une interruption peut être déclenchée par votre matériel. C'est ce qui arrive lorsque vous appuyez sur une touche du clavier. Aucun logiciel n'intervient et le contrôle est passé directement à la routine qui gère le clavier : ce sont les interruptions matérielles.

Les interruptions logicielles sont quant à elles appelées par des instructions en langage machine au sein d'un programme. Leur importance est capitale. Rappelez-vous que contrairement au PASCAL ou au C, l'assembleur ne dispose pas de fonction préprogrammée. Chaque instruction doit être directement traduisible en langage machine.

Mais alors, comment fait-on pour écrire une chaîne de caractères à l'écran ? Ou bien pour lire un caractère entré au clavier ?

Eh bien on le fait de la même façon que le DOS lui-même ! On déclenche les interruptions appropriées à l'aide de l'instruction « INT » du langage machine. C'est donc une routine du DOS (ou parfois du BIOS) qui fera tout le travail. Les paramètres (ou leurs adresses) sont passés dans les registres.

Voici un petit exemple en assembleur qui écrit la lettre ‘A' à l'écran :

Image non disponible

Examinons-le ligne par ligne :

  • l'instruction « MOV DL, ‘A' » demande au processeur de mettre dans le registre DL le code ASCII de la lettre ‘A', c'est-à-dire 65, ou 41h ;
  • « MOV AH, 02 » : mettre le nombre 2 dans AH ;
  • enfin, la dernière instruction appelle l'interruption numéro 21h. Il existe 256 interruptions. Toutes sont notées en base hexadécimale.

Explications :

Comme vous aurez très vite l'occasion de vous en rendre compte, l'interruption 21h est l'interruption du DOS par excellence. Elle permet d'appeler de nombreuses fonctions très diverses. Pour cela, il suffit de mentionner leur numéro dans le registre AH.

Il est très difficile de mémoriser le rôle de chaque interruption, et a fortiori de chaque fonction ou sous-fonction, d'autant plus qu'elles sont désignées par des numéros hexadécimaux et qu'elles attendent des paramètres dans des registres précis. C'est pourquoi tout programmeur se doit d'avoir à sa disposition une liste des interruptions pour travailler. Celle de Ralph Brown est très connue, et vous la trouverez sur l'Internet.

Revenons à notre exemple. La fonction numéro 2 de l'interruption 21h sert à écrire un caractère à l'écran. Il faut pour cela écrire le code ASCII du caractère dans le registre DL et bien sûr placer le nombre 2 dans AH.

Une fois que AH et DL ont été ajustés, l'interruption 21h peut être appelée à l'aide de l'instruction INT.

D'autres interruptions ne remplissent qu'une seule tâche. Vous n'avez donc pas besoin de mettre un numéro de fonction dans AH. L'appel de l'interruption suffit.

II-D-2. La table des vecteurs d'interruptions

À chaque appel d'interruption, l'ordinateur doit pouvoir trouver l'adresse de l'ISR correspondante. Pour cela, il dispose de la table des vecteurs d'interruptions (TVI, ou IVT : « Interrupt Vector Table »). Cette table est implantée à l'adresse 0000:0000 c'est-à-dire au début de la RAM.

La table est organisée comme suit :

Adresse | Taille (octets) | Valeur
0000:0000 | 2 | Adresse d'offset de l'interruption numéro 0
0000:0002 | 2 | Adresse de segment de l'interruption numéro 0
0000:0004 | 2 | Adresse d'offset de l'interruption numéro 1
0000:0006 | 2 | Adresse de segment de l'interruption numéro 1
0000:0008 | 2 | Adresse d'offset de l'interruption numéro 2
0000:000A | 2 | Adresse de segment de l'interruption numéro 2
0000:000C | 2 | Adresse d'offset de l'interruption numéro 3
0000:000E | 2 | Adresse de segment de l'interruption numéro 3
etc. etc. etc.
0000:03FC | 2 | Adresse d'offset de l'interruption numéro 255
0000:03FE | 2 | Adresse de segment de l'interruption numéro 255

La raison pour laquelle les offsets précèdent les adresses de segment vient du codage en « little endian » utilisé par INTEL. Les processeurs de cette marque (contrairement à la plupart des autres, qui travaillent en « big endian ») ont une représentation des données en mémoire aussi curieuse qu'insupportable pour le programmeur : ils placent le poids fort après le poids faible. Ainsi, le mot 4A28h sera codé 284Ah. Puisque l'adresse de l'ISR tient sur 32 bits (16 + 16), elle est représentée comme un double-mot (« dword »), donc l'adresse d'offset, qui est le mot de poids faible, se trouve au début. Eh oui, c'est pénible, mais il faudra s'y faire !

Remarque : pour calculer l'adresse de l'entrée dans la TVI correspondant à l'interruption numéro X, il suffit de multiplier X par 4.

Exemple : l'adresse de l'ISR numéro 21h est stockée à 0000:0084.

II-D-3. Sauvegarde de l'état des registres lors de l'appel

Un appel d'interruption obéit à certaines règles, car il est indispensable, une fois l'ISR exécutée, que le programme interrompu retrouve les registres dans le même état qu'ils étaient auparavant. Tout doit se passer comme si l'interruption n'avait jamais eu lieu. C'est pourquoi avant de faire un saut à l'ISR pointée par l'entrée correspondante dans la TVI, l'ordinateur sauvegarde tous les registres sur la pile courante. Il restaurera leur contenu avant de rendre le contrôle. Cette procédure est automatique. Il n'incombe pas au programmeur de prendre toutes ces précautions.

III. DEUXIÈME PARTIE

Cette partie a pour but de vous présenter l'architecture du langage assembleur à travers de courts exemples. Ne vous inquiétez pas si vous ne comprenez pas parfaitement certaines instructions : toutes seront détaillées dans la troisième partie. L'essentiel est que vous compreniez grossièrement ce qu'on fait et pourquoi on le fait . Les explications qui accompagnent ces exemples devraient y suffire. Vous pourrez ensuite apprendre le langage en lui-même en consultant la partie suivante.

Remarques pratiques préliminaires

  • Les mots qui sont imprimés en italiques sont ceux qui ne font pas partie du langage en lui-même. Ils sont choisis par le programmeur.
  • Tous les programmes présentés ci-dessous suivent la syntaxe du compilateur TASM. Il existe de très légères différences d'un compilateur à l'autre.
  • Le code peut être écrit en majuscules ou en minuscules. Chaque mot est séparé des autres par des espaces ou des tabulations.
  • Les commentaires sont précédés du signe ‘;'.
  • Les nombres doivent toujours commencer par un chiffre (entre 0 et 9), même les nombres hexadécimaux. Il faut donc écrire 0F2Ah et non pas F2Ah. Par contre, l'écriture 5CFh est correcte.
  • Les apostrophes et les guillemets sont équivalents. Il est cependant plus commode de réserver l'usage des guillemets aux chaînes de plusieurs caractères et celui des apostrophes aux caractères isolés.
  • Vous pouvez taper vos programmes avec l'éditeur de texte du DOS (EDIT.COM). Donnez-leur l'extension ‘.asm'.
  • La compilation se fait en tapant “TASM /m9 MONPROG” si votre source s'appelle ‘MONPROG.ASM'.

Le paramètre ‘/m9', facultatif, indique au compilateur qu'il devra effectuer 9 passes, c'est-à-dire qu'il examinera 9 fois le code source. Comme chaque examen se fait de manière linéaire, le compilateur trouve parfois des instructions qui font référence à des labels placés plus loin dans le programme. Il manque donc d'informations, mais il continue son examen jusqu'à la fin. Quand il a terminé, il recommence tout depuis le début pour résoudre les problèmes qui s'étaient posés.

La compilation crée un fichier objet (‘.obj'). Pour obtenir un fichier EXE, tapez ”TLINK MONPROG”. Pour un fichier COM, tapez “TLINK /tdc MONPROG”.

Après l'édition des liens (le « linkage »), vous pouvez supprimer les fichiers MONPROG.obj' et ‘MONPROG.map'.

  • Il est possible de créer un fichier BAT qui s'occupe de toutes ces étapes. Ouvrez l'éditeur EDIT du DOS et tapez un programme tel que celui-ci :

Enregistrez-le et nommez-le MAKE.BAT. Vous pouvez compiler et linker en tapant “MAKE MONPROG”. Pour compiler des fichiers COM, ajoutez le paramètre '/tdc' à la commande TLINK.

Sélectionnez

@ECHO OFF
TASM /m9 %1.asm
IF NOT EXIST %1.obj GOTO FIN
TLINK %1.obj REM : ajouter ici /tdc pour obtenir un fichier COM
ERASE %1.map
ERASE %1.obj
:FIN
  • Pour pouvoir lancer TASM et TLINK quel que soit le dossier courant, changez la variable PATH ainsi :

PATH = %path%;c:\MonChemin\DossierTASM

Vous pouvez inclure cette ligne à la fin de votre fichier AUTOEXEC.BAT afin qu'elle soit exécutée à chaque démarrage.

  • Vous pouvez déboguer vos programmes (par exemple les exécuter instruction par instruction en observant les changements induits dans la RAM et dans les registres…) avec le logiciel Turbo Debugger 16/32 bits qui est très pratique et très performant. À défaut, vous pouvez vous rabattre sur l'archaïque DEBUG.COM (il se trouve dans votre dossier de commandes DOS). Mais alors bon courage !

Le seul travail du compilateur (et du linkeur) est de convertir chacune de vos instructions en son équivalent en langage machine. Le programme compilé présentera donc exactement la même structure et la même linéarité que votre code source .

III-A. PREMIER EXEMPLE : LES FICHIERS COM

Voici un petit programme COM qui écrit le message « Bonjour, monde ! » à l'écran.

Image non disponible

Ce code source commence par des directives. Une directive est une information que le programmeur fournit au compilateur. Elle n'est pas transformée en une instruction en langage machine. Elle n'ajoute donc aucun octet au programme compilé.

La directive “.386“ indique au compilateur que le programme est destiné à tourner sur des processeurs INTEL de modèle 386 (ou supérieur). Cela nous autorise à utiliser certaines instructions qui ne sont pas disponibles sur les modèles antérieurs, comme PUSHA ou POPA. Dans cet exemple, cette directive aurait très bien pu être omise.

La ligne

code segment use16

sert à déclarer un segment que l'on appelle “code”. On aurait tout aussi bien pu le nommer “marteau” ou “voiture”. Ce sera le segment de notre programme. N'oubliez pas qu'un fichier COM ne peut comporter qu'un seul segment. Cette ligne ne sera pas compilée : elle ne sert qu'à indiquer au compilateur le début d'un segment.

Le mot “use16” indique que les adresses de segment et d'offset sont codées sur 16 bits et non sur 8 bits. Vous devez systématiquement l'écrire.

La directive

assume cs:code, ds:code, ss:code

informe le compilateur que tout au long du programme, CS, DS et SS pointeront de façon privilégiée vers le segment “code”, ce qui est évident d'ailleurs puisque c'est le seul segment… Vous comprendrez le sens exact de “assume” dans la troisième partie du cours.

Enfin, les mots

org 100h

signifient qu'il faudra ajouter 100h (soit 256) à tous les offsets. Pourquoi ? Souvenez-vous de la structure d'un programme COM en mémoire. Si vous n'écrivez pas cette ligne, TASM considérera que le programme débute à l'offset 0000. Or, lors de l'exécution, le DOS le chargera après le PSP, c'est-à-dire à l'adresse 100h. C'est pourquoi il est nécessaire de recalculer les offsets : l'offset 0000 deviendra 0100. Cette directive est en quelque sorte le trait caractéristique des fichiers COM.

Nous en arrivons au programme proprement dit. Il commence par un label :

debut :

Lui non plus n'est pas compilé. Il ne sert qu'à représenter l'adresse de l'instruction qui le suit, c'est-à-dire :

mov ah, 09h

Cette ligne demande au processeur de charger la valeur 9 dans le registre AH. C'est le numéro de la fonction de l'interruption 21h qui écrit une chaîne de caractères à l'écran. L'offset de cette chaîne est attendu dans DX. D'où la ligne suivante :

mov dx, offset message

Le mot-clé “offset” sert à extraire l'offset du label “message” qui représente quant à lui l'adresse du message à écrire (il contient donc une adresse de segment ET un offset). L'adresse de segment de la chaîne doit être transmise dans DS. Mais il est inutile de changer ce registre, car il pointe déjà vers notre segment.

Il nous reste à appeler l'interruption 21h :

int 21h

et à rendre la main au DOS :

ret

Lorsque nous aborderons les procédures, vous comprendrez mieux le sens de ce mot et ce qu'il fait exactement. Pour l'instant, sachez simplement que seul un fichier COM peut se terminer avec cette instruction.

Nous arrivons à la ligne :

message db “Bonjour, monde !”, ‘$'

Il s'agit d'une définition de données. Le mot “db” (« define byte ») signifie que le compilateur devra écrire les octets qui suivent tels qu'ils sont dans notre code source. Il va donc écrire le code ASCII du ‘B', puis celui du ‘o', etc. Il terminera en écrivant le code ASCII du signe ‘$'. C'est ainsi que la fonction 9 de l'interruption 21h reconnaît la fin de la chaîne à écrire. Si vous oubliez ce signe, elle écrira tous les octets de la RAM jusqu'à ce qu'elle tombe par hasard sur lui.

Le mot “message” placé en début de ligne est un label de données. Il représente l'adresse du code ASCII du ‘B'. Remarquez qu'il n'y a pas de caractère ‘:' après un label de données.

La ligne

code ends

indique la fin du segment « code », et enfin

end debut

informe le compilateur que le fichier est fini, tout comme le “END.” du PASCAL. Le nom du label « debut » est mentionné : ce sera le point d'entrée de notre programme. C'est vers lui que pointera CS:IP avant l'exécution.

Remarque : vous n'êtes pas tenu de rendre aux registres la valeur qu'ils avaient au début de votre programme. De toute façon, avant de charger un programme, le DOS sauvegarde le contenu de tous les registres puis met le contenu des registres généraux (ainsi que SI, DI et BP) à zéro. Il les restaurera quand vous lui rendrez la main.

III-B. DEUXIÈME EXEMPLE : LES FICHIERS EXE

Le programme suivant est un programme EXE qui fait exactement la même chose que l'exemple précédent.

Image non disponible

Examinons-le !

Tout d'abord, la directive

org 100h

a disparu.

En effet, lors de l'exécution, CS pointera vers notre segment de code et non pas vers le PSP. Il est donc inutile de décaler les offsets de 256.

Remarquons que notre programme dispose de deux segments supplémentaires :

  • le segment « data » est destiné à contenir les données, c'est-à-dire les variables ;
  • le segment « pile » sera notre segment de pile.

Notre directive « assume » devient donc :

assume cs:code, ds:data, ss:pile

Ainsi, le compilateur est informé que DS pointera vers le segment « data » et SS vers « pile ».

Les deux lignes suivantes,

mov ax, data

mov ds, ax

servent à initialiser le registre DS. Celui-ci pointe vers le PSP au début du programme, mais nous voulons le faire pointer vers notre segment de données appelé « data ». Cela est nécessaire puisque la fonction 9 de l'interruption 21h attend l'adresse de la chaîne dans le couple DS:DX et que notre message se trouve dans le segment de données.

La première instruction charge l'adresse du segment « data » dans AX. La seconde transfère cette valeur de AX dans DS.

Mais pourquoi diable utiliser AX comme intermédiaire ? Après tout, on pourrait écrire :

mov ds, data

Eh bien non ! Pour la simple raison que DS est un registre de segment et qu'en tant que tel on ne peut pas lui charger de valeur immédiate.

On appelle « valeur immédiate » toute constante tapée directement dans l'instruction elle-même.

Exemples de chargement de valeurs immédiates :

mov ax, 135 ;charge 135 dans AX

mov bx, offset message ;charge l'offset de message dans BX

mov bx, offset fin - offset debut ;charge le nombre d'octets entre fin et debut dans BX

mov es, 10 ;instruction illicite, car ES est un registre de segment !

Remarque : une autre possibilité aurait été d'écrire : “PUSH AX” (empiler AX) puis “POP DS” (dépiler le dernier nombre empilé et le placer dans DS).

Les trois lignes suivantes :

mov ah, 09h

mov dx, offset message

int 21h

n'ont pas changé.

Il nous faut également terminer le programme par un appel de la fonction 4ch de l'interruption 21h. C'est ainsi que se terminent les programmes EXE.

mov ah, 4ch

int 21h

Remarque : on aurait également pu écrire :

mov ax, 4c00h

int 21h

La seule différence est que AL est mis à zéro, ce qui indique au programme à qui on rend le contrôle (ici le DOS) que notre programme s'est terminé normalement. Les fichiers COM peuvent également utiliser la fonction 4ch.

Le segment de code se termine :

code ends

et le segment de données commence à sa suite :

data segment use16

message db “Bonjour, monde !”, ‘$'

data ends

Il nous reste à écrire le segment de pile. Dans ce programme, il n'était pas absolument indispensable de le séparer du segment de code. Mais c'est une bonne habitude de le faire.

pile segment stack

remplissage db 256 DUP (?)

pile ends

Le mot-clé « stack » indique que ce segment est le segment de pile.

Les mots « db 256 DUP (?) » déclarent 256 octets non initialisés. C'est la « matière » de notre pile.

Sachez que tout appel d'interruption se traduit par l'empilage des flags et de CS:IP. Il est donc indispensable d'avoir une pile, même si celle-ci peut éventuellement partager le même segment que le code, comme dans un fichier COM. Mais dans un programme EXE, il vaut mieux réserver un segment à la pile. Les 256 octets que nous déclarons ici indiquent seulement que la pile contient 256 octets. Ainsi, au début de l'exécution, SS:SP pointera vers la fin de ces octets. Nous verrons ce que signifie le point d'interrogation dans la troisième partie.

La fin du fichier et le point d'entrée sont signalés par :

end debut

Et voilà ! Vous avez découvert l'allure d'un programme en assembleur. Nous pouvons donc à présent nous pencher sur l'étude du langage.

IV. TROISIÈME PARTIE

IV-A. DÉFINITION DE DONNÉES ET ADRESSAGE

IV-A-1. Les définitions de données

Les mots “db” (« define byte »), “dw” (« define word »), ”dd” (« define double word ») permettent de déclarer et d'initialiser une variable. Il faut bien comprendre que leur seule action est d'écrire les données dans l'exécutable à l'endroit même où elles se trouvent dans le code source.

Pour accéder à ces données (appelées « variables »), il suffit de connaître leur adresse. Pour cela, on peut les faire précéder d'un label de données.

Exemple :

TOTO dw 1982

Dans la ligne précédente, TOTO représente l'adresse du nombre 1982 qui est défini juste après. Comme ce nombre est codé sur deux octets (word = 16 bits), c'est le premier octet qui est adressé par TOTO.

Le compilateur se contentera d'écrire le nombre 1982 à l'endroit où se trouve la déclaration.

Remarque : en C, on écrirait ceci :

int TOTO = 1982;

En BASIC ou en PASCAL, cette définition n'a pas d'équivalent, puisque les variables ne peuvent être initialisées lors de leur déclaration. Il est donc obligatoire d'ajouter une ligne de code pour le faire.

Dans la partie précédente, nous avons eu à définir le message qui serait affiché à l'écran. Voici la ligne que nous avons écrite :

message db “Bonjour, monde !”, ‘$'

Lorsqu'il rencontre une chaîne de caractères, le compilateur écrit les codes ASCII de tous les caractères, les uns à la suite des autres.

Voici une autre manière d'écrire cette définition de données. Le résultat (i.e. le programme compilé) est EXACTEMENT LE MÊME :

message db 42h, 111, ‘nj'

db ‘our, mo'

db ‘nde !$'

Pour définir plusieurs fois à la suite les mêmes données, on utilise “DUP” (« duplicate ») de la manière suivante :

TOTO db 100 dup(“TOTO EST BEAU”)

Cela revient à écrire :

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

TOTO db “TOTO EST BEAU”

;… etc. (100 fois) …

Il est fréquent que de nombreuses données n'aient pas besoin d'être initialisées à une valeur précise. Dans ce cas, on les regroupe à la fin du programme et on les remplace par le caractère ‘?'. Les adresses des labels seront calculées de la même façon, mais aucune donnée ne sera écrite dans le fichier (et a fortiori dans la RAM). On dit que l'on met ses variables sur le « tas » (« heap » en anglais). La seule différence avec une définition classique est qu'au début de l'exécution, nos variables n'auront pas de valeur définie. Leurs valeurs seront « aléatoires » en ce sens qu'on ne peut les connaître au moment de la compilation.

Exemple :

Image non disponible

Remarque : Si elles ne sont pas regroupées en fin de programme, le compilateur sera obligé d'écrire les données dans le fichier afin de ne pas fausser les adresses des variables (ou du code) qui suivent. Il remplira alors les points d'interrogation avec des zéros.

Pour indiquer qu'un nombre est noté en base hexadécimale, on lui ajoute la lettre ‘h'. La lettre ‘b' signifie que le nombre est codé en binaire, la lettre 'o' en octal et la lettre ‘d' en base décimale (base par défaut).

IV-A-2. L'adressage

IV-A-2-a. L'adressage immédiat

On appelle « adressage immédiat » l'adressage qui ne fait intervenir que des constantes.

Considérons cette partie de programme qui stocke le nombre 125h dans une variable appelée TOTO, lui ajoute 15 puis charge le résultat dans AX.

Image non disponible

La ligne

mov word ptr ds:[TOTO], 125h

charge le nombre 125h dans le mot de la RAM adressé par DS et l'offset de “TOTO”. La ligne suivante ajoute 15 au contenu de ce mot.

L'expression “word ptr” devant l'adresse, obligatoire ici, indique la taille de la variable dans laquelle doit être stocké le nombre 125h.

Si on avait mis “dword ptr”, ce nombre aurait été codé sur 32 bits (00000125h) au lieu de 16 bits (0125h) : on aurait donc écrasé les deux octets qui suivent la variable. Si on avait mis “byte ptr”, la compilation aurait été impossible, car un octet ne peut contenir un nombre supérieur à FFh.

Le compilateur n'a en effet aucun moyen de connaître cette taille. La variable “TOTO” n'a pas de taille (malgré le mot “dw”) : ce n'est en fait qu'un pointeur vers le premier octet du word qu'elle représente.

En revanche, la troisième instruction ne fait pas apparaître l'expression “word ptr”. Cette fois-ci elle est facultative du fait que la destination (AX = 16 bits) impose la taille.

On peut évidemment remplacer “TOTO” par une constante numérique :

mov word ptr ds:[004Ch], 125h

Le mot 125h sera alors écrit à l'adresse DS:4C.

Une question se pose à présent : que se passe-t-il si le programmeur n'écrit pas le registre de segment dans l'adresse ?

C'est là qu'intervient la directive “assume”. Le compilateur va devoir trouver lui-même quel registre l'utilisateur a voulu sous-entendre.

Si on a utilisé un label, alors le segment sera celui dans lequel est déclaré le label. Mais le compilateur veut un REGISTRE de segment. Il va donc prendre celui qui est censé pointer vers le bon segment et pour le savoir, il examine la directive assume.

Voilà pourquoi cette dernière peut nous épargner d'écrire pour chaque variable l'expression “ds:”.

Comprenez bien que cette directive ne sert à rien d'autre qu'à cela et qu'en aucune façon elle ne force les registres de segment à prendre quelque valeur que ce soit.

IV-A-2-b. L'adressage indexé et/ou basé

Comme nous l'avons déjà expliqué, il est possible d'utiliser des registres de base ou d'index pour adresser un octet.

On appelle « base » les registres BX et BP et « index » les registres SI et DI. La différence est que la base est censée être fixe tandis que l'index varie automatiquement lorsqu'on utilise certaines opérations, telles que MOVSB.

Remarque : BX, BP, DI et SI peuvent tous être utilisés comme des registres généraux.

Voici les adressages possibles :

  • Constante seule (= adressage immédiat) :

Exemples :

MOV AH, byte ptr [TOTO]

MOV BX, word ptr ds:[1045h]

MOV ES:[10 + TOTO x 2], BL

  • Base seule :

Exemple :

MOV dword ptr ds:[BP], 15

  • Index seul :

Exemple :

MOV dword ptr es:[DI], 142

  • Base + Constante :

Exemples :

MOV byte ptr ds:[BP + 1], 12

MOV ds:[BX + (TOTO - BOBO)/10], AX

  • Index + Constante :

Exemples :

MOV CX, [TOTO + DI]

MOV AL, ds:[1 + SI]

  • Base + Index :

Exemple :

MOV ds:[BP + SI], AH

  • Base + Index + Constante :

Exemple :

MOV AX, word ptr [TOTO + BX + DI + 1]

Remarque : si la constante n'est pas un label, il est parfois impératif de spécifier le registre de segment ! Tout dépend du contexte…

IV-B. SAUTS INCONDITIONNELS, PROCÉDURES ET MACROS

IV-B-1. Les sauts inconditionnels

Ce paragraphe répond à la question : « Mais comment fait-on un GOTO en assembleur ? ».

L'instruction équivalente est JMP qui signifie « jump ».

La syntaxe est

JMP MonLabel

La référence à l'étiquette MonLabel sera remplacée lors de la compilation par la distance (signée) en octets qui sépare l'instruction qui suit immédiatement le JMP de l'instruction adressée par MonLabel. En pratique, le JMP s'utilise exactement comme un GOTO en BASIC.

Cette instruction permet de faire un saut inconditionnel : le programme fera ce saut quoi qu'il arrive.

Exemple :

Image non disponible

Le mot “short” ajouté après “jmp” indique au compilateur que le label “COUCOU” se trouve à une distance (signée) qui peut être stockée sur un seul octet. Le compilateur ne le sait pas encore lorsqu'il essaie de compiler l'instruction de saut, car le label se trouve en deçà de cette instruction. C'est pourquoi il prévoit deux octets pour écrire le saut, au cas où l'adresse d'arrivée est éloignée de plus de 128 octets. Lorsqu'il effectue une deuxième « passe », s'il s'aperçoit qu'un seul octet aurait suffi il remplit alors l'octet inutile avec l'instruction NOP (« No Operation ») qui ne fait rien du tout. Le programme marchera parfaitement (encore heureux !), mais cela gaspille un octet. Le programmeur a la possibilité d'aider le compilateur en ajoutant le mot “short”, ainsi un seul octet est prévu pour la distance de saut. Si le label est placé plus haut que l'instruction de saut, il est inutile de l'écrire.

Remarque : Ce mot peut également être utilisé avec les instructions de saut conditionnel que nous étudierons plus loin.

IV-B-1-a. Les procédures

IV-B-1-a-i. appels de procédures

Ceux qui ont déjà programmé en BASIC connaissent les instructions GOSUB et RETURN. Leurs équivalents en assembleur sont CALL et RET.

  • L'instruction CALL sert à appeler une procédure :

Syntaxe :

CALL MonLabel

Action : empile l'offset de l'instruction suivante puis fait un simple saut à l'adresse représentée par MonLabel.

  • RET (ou RETN) permet de retourner à l'instruction qui suit immédiatement le CALL :

Syntaxe :

RET

Action : dépile l'adresse de retour et la met dans IP. Le programme continue donc à l'adresse qui suit le CALL.

Remarque : Rappelez-vous qu'il est possible de terminer un programme COM avec l'instruction RET. Puisque le DOS empile un zéro de deux octets au chargement du programme, IP prendra la valeur 0000 lorsque le processeur exécutera cette instruction : il pointera donc vers le début du PSP. Or le PSP commence toujours par les deux octets suivants : CDh 20h (ce qui s'écrit INT 20h en assembleur). L'interruption 20h, qui permet de terminer un programme COM, sera donc appelée et le contrôle sera rendu au DOS.

Voici un exemple d'utilisation des procédures aussi simple que possible : ce programme COM appelle 12 fois une procédure qui écrit un message à l'écran et rend la main au DOS. Il n'est d'aucune utilité et n'est pas optimisé du tout.

Image non disponible

Remarque : Les codes ASCII 10 et 13 représentent respectivement la fin de ligne et le retour chariot. Grâce à eux, on revient à la ligne chaque fois qu'on a écrit le message.

Ce programme fonctionne parfaitement. Toutefois, les conventions d'écriture veulent que les procédures soient écrites comme suit :

Image non disponible

Le programme compilé est toujours le même, mais le code est plus lisible puisqu'on distingue les procédures des simples labels. Le mot “near” signifie que l'adresse de cette procédure est réduite à un offset. Il n'y a pas d'adresse de segment. La procédure ne pourra donc être appelée que de l'intérieur même du segment.

Le mot qui a le sens contraire de “near” est “far”. Les « call far » sont assez peu utilisés.

IV-B-1-a-ii. le passage des paramètres

Il existe deux moyens de passer des paramètres à une procédure : les registres et la pile.

  • Le passage par registres :

Il consiste à transmettre les paramètres dans des registres convenus à l'avance, par exemple AX et BX. Les deux gros avantages de cette méthode sont sa simplicité et la vitesse d'exécution. L'inconvénient majeur est le nombre limité de registres, d'autant plus qu'au moment de l'appel, certains d'entre eux ne seront peut-être pas disponibles.

  • Le passage par la pile :

C'est la méthode qu'utilisent les langages de haut niveau tels que le C ou le PASCAL. Avant l'appel, on empile les paramètres un à un. La procédure appelée se chargera de les lire.

Pour cela on utilise le registre BP de la manière suivante : on transfère la valeur de SP dans BP à l'aide de l'instruction “MOV BP, SP”. L'adresse de retour (qui sera dépilée quand le processeur rencontrera l'instruction “RET”) se trouve à l'adresse SS:[BP], le dernier paramètre est adressé par SS:[BP + 2], l'avant-dernier par SS:[BP + 4], etc. On peut ainsi lire n'importe quel paramètre sans le dépiler.

Attention cependant ! À la fin de la procédure, il faudra tout de même rendre à SP la valeur qu'il avait avant l'empilage des paramètres. Pour cela, on fait suivre l'instruction RET du nombre de paramètres, multiplié par 2 (car chaque paramètre comporte deux octets).

Exemple :

Image non disponible

Soyez toujours très vigilant avec les appels de procédures : pensez que l'adresse de retour sera dépilée lorsque le programme rencontrera l'instruction RET. Il serait donc suicidaire de laisser une donnée empilée avant d'appeler cette instruction.

IV-B-1-b. Les macros

Étant donné que certaines instructions se répètent constamment dans un programme, l'écriture de macrofonctions (ou macros) est un moyen pratique de rendre votre code source plus lisible.

Il est possible de choisir pour certaines suites d'instructions un nom qui les représente. Lorsque le compilateur (en fait, le préprocesseur) rencontrera ce nom dans votre code source, il le remplacera par les lignes de code qu'il désigne. Ces lignes forment une « macro ».

Les macros, à la différence des procédures, n'ont aucune signification pour la machine. Seul le compilateur comprend leur signification. Elles ne sont qu'un artifice mis à la disposition du programmeur pour clarifier son programme. Lorsque le compilateur rencontre le nom d'une macro dans votre code, il le remplace par le code de la macro. Tout se passe exactement comme si vous aviez tapé vous-même ce code à la place du nom de la macro.

Ainsi, si vous appelez quinze fois une macro dans votre programme, le compilateur écrira quinze fois le code de cette macro. C'est toute la différence avec les procédures qui ne sont écrites qu'une seule fois, mais peuvent être appelées aussi souvent qu'on veut à l'aide d'un CALL.

Voici comment écrire une macro : l'exemple suivant sert à écrire un message à l'écran.

Image non disponible

Le code précédent peut être écrit n'importe où dans votre programme, à condition qu'il se trouve avant tout appel de cette macro. Afin d'éviter les ennuis, il est fortement conseillé de réunir vos macros au début du code, avant toute autre ligne. De toute façon, il ne sera pas compilé à l'endroit où vous l'avez écrit, mais aux endroits où se trouvent les appels de macros.

Le mot “text?” est un « paramètre ». Le point d'interrogation n'est pas requis ; nous l'avons mis pour indiquer qu'il s'agit d'un paramètre et non d'une variable. Une macro peut utiliser plusieurs paramètres séparés par des virgules.

Pour appeler cette macro, il vous suffit d'écrire la ligne

ecrit_texte “Coucou ! Ceci est un essai !”

Le compilateur se chargera alors de la remplacer par les instructions comprises entre la première et la dernière ligne de cet exemple, en prenant le soin de remplacer le mot “text?” par le message fourni en paramètre.

En assembleur, chaque label doit avoir un nom unique. Or, si une macro est appelée plusieurs fois, les mêmes noms seront utilisés. Il faut donc la plupart du temps inclure la directive LOCAL qui forcera le compilateur à changer le nom des labels à chaque appel de la macro. Attention : cette directive doit suivre immédiatement la déclaration de la macro.

Supposons à présent que l'on veuille écrire à l'écran le message « Je suis bien content » et revenir à la ligne à l'aide de notre macro ecrit_texte.

La syntaxe suivante :

ecrit_texte “Coucou ! Ceci est un essai !”, 10, 13

est incorrecte, car le compilateur croirait que l'on a écrit trois paramètres ! Il faut alors entourer notre unique paramètre par les signes ‘<' et ‘>' :

ecrit_texte <”Coucou ! Ceci est un essai !”, 10, 13>

IV-B-1-c. La directive EQU

La directive EQU a un rôle voisin de celui des macros. Elle permet de remplacer un simple mot par d'autres plus complexes. Son intérêt est qu'elle peut être invoquée en plein milieu d'une ligne.

Quelques exemples :

Longueur EQU (fin - debut)

Message EQU “Bonjour messieurs ! Comment allez-vous ?”, ‘$'

Version EQU 2

Quitter EQU ret

Quitter2 EQU int 20h

Mettre_dans_AH EQU mov ah,

Interruption_21h EQU int 21h

Un programme qui contient de telles directives peut se terminer ainsi :

Mettre_dans_AH 4Ch

Interruption_21h

Ou ainsi (si c'est un COM) :

Quitter2

IV-B-1-d. L'inclusion de fichiers

Il est possible d'accéder à des procédures, des macros ou des définitions EQU qui se trouvent dans d'autres fichiers. Cela permet de se constituer des bibliothèques de macros ou de procédures que l'on peut réutiliser d'un programme à l'autre.

Pour inclure le fichier TOTO.LIB, écrivez au début de votre code source :

Image non disponible

La condition “if1” indique au compilateur que l'inclusion ne doit s'effectuer que lors de la première passe.

Le fichier TOTO.LIB est un fichier texte tout à fait banal qui contient des lignes de code en assembleur.

IV-C. LES PRINCIPALES INSTRUCTIONS DU LANGAGE MACHINE

Remarques préliminaires :

  • Le principe du langage assembleur est de remplacer chaque opcode hexadécimal par un mot facile à retenir. Ce mot est appelé mnémonique. Par exemple, “INT” est le mnémonique associé à l'opcode CDh. Chaque fois que le compilateur rencontrera ce mot, il le remplacera par l'octet CDh et écrira ensuite l'opérande (ici : le numéro de l'interruption) en hexadécimal.
  • Cette liste récapitule les instructions que nous connaissons déjà et en présente de nouvelles. Elle n'est pas exhaustive, mais vous sera amplement suffisante pour la plupart de vos programmes.
  • Certaines instructions, comme PUSHA, ne sont disponibles que pour des modèles de processeurs plus évolués que le 8086, par exemple le 286. N'oubliez pas la directive .386 si vous les utilisez.

IV-C-1. L'instruction NOP (« No Operation »)

Syntaxe : NOP

Description : Ne fait rien ! Mais alors RIEN ! Que dalle ! Niet !

IV-C-2. L'instruction MOV (« Move »)

Syntaxe : MOV Destination, Source

Description : Copie le contenu de Source dans Destination.

Mouvements autorisés :

MOV Registre général, Registre quelconque

MOV Mémoire, Registre quelconque

MOV Registre général, Mémoire

MOV Registre général, Constante

MOV Mémoire, Constante

MOV Registre de segment, Registre général

Remarques : Source et Destination doivent avoir la même taille. On ne peut charger dans un registre de segment que le contenu d'un registre général (SI, DI et BP sont considérés ici comme des registres généraux).

Exemples :

MOV AX, 5

MOV ES, DX

MOV AL, [Variable1] ; Copie un octet, car AL contient 8 bits

MOV [Variable2], DS ; Copie un word car DS contient 16 bits

MOV word ptr [Variable3], 12 ; Ici, on spécifie que la variable est un word

IV-C-3. L'instruction XCHG (« Exchange »)

Syntaxe : XCHG Destination, Source

Description : Échange les contenus de Source et de Destination.

Mouvements autorisés :

XCHG Registre général, Registre général

XCHG Registre général, Mémoire

XCHG Mémoire, Registre général

IV-C-4. L'instruction JMP (« Jump »)

Syntaxe : JMP MonLabel

Description : Saute à l'instruction pointée par MonLabel.

IV-C-5. L'opérateur CMP (« Compare »)

Syntaxe : CMP Destination, Source

Description : Cet opérateur sert à comparer deux nombres : Source et Destination. C'est le registre des indicateurs qui contient les résultats de la comparaison. Ni Source ni Destination ne sont modifiés.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : Cet opérateur effectue en fait une soustraction, mais contrairement à SUB, le résultat n'est pas sauvegardé.

Le programme doit pouvoir réagir en fonction des résultats de la comparaison. Pour cela, on utilise les sauts conditionnels (voir ci-dessous).

IV-C-6. Les instructions de saut conditionnel

Les sauts conditionnels sont terriblement importants, car ils permettent au programme de faire des choix en fonction des données.

Un saut conditionnel n'est effectué qu'à certaines conditions portant sur les flags (par exemple : CF = 1 ou ZF = 0).

Certains mnémoniques de sauts conditionnels sont totalement équivalents, c'est-à-dire qu'ils représentent le même opcode hexadécimal. C'est pour aider le programmeur qu'ils existent parfois sous plusieurs formes.

IV-C-6-a. les sauts de comparaison

  • JE (« Jump if Equal ») fait un saut au label spécifié si et seulement si ZF = 1. Rappelez-vous que ce flag est à 1 si et seulement si le résultat de l'opération précédente vaut zéro. Comme CMP réalise une soustraction, on utilise généralement JE pour savoir si deux nombres sont égaux.

Exemple :

Image non disponible

Mnémonique équivalent : JZ (« Jump if Zero »)

  • JG (« Jump if Greater ») fait un saut au label spécifié si et seulement si ZF = 0 et SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur à un autre.

Exemple:

Image non disponible

Mnémonique équivalent : JNLE (« Jump if Not Less Or Equal »)

  • JGE (« Jump if Greater or Equal ») fait un saut au label spécifié si et seulement si SF = OF. On l'utilise en arithmétique signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNL (« Jump if Not Less »)

  • JL (« Jump if Less ») fait un saut au label spécifié si et seulement si SF <> OF. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNGE (« Jump if Not Greater Or Equal »)

  • JLE (« Jump if Less Or Equal ») fait un saut au label spécifié si et seulement si SF <> OF ou ZF = 1. On l'utilise en arithmétique signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNG (« Jump if Not Greater »)

  • JA (« Jump if Above ») fait un saut au label spécifié si et seulement si ZF = 0 et CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur à un autre.

Mnémonique équivalent : JNBE (« Jump if Not Below Or Equal »)

  • JAE (« Jump if Above or Equal ») fait un saut au label spécifié si et seulement si CF = 0. On l'utilise en arithmétique non signée pour savoir si un nombre est supérieur ou égal à un autre.

Mnémonique équivalent : JNB (« Jump if Not Below »)

  • JB (« Jump if Below ») fait un saut au label spécifié si et seulement si CF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur à un autre.

Mnémonique équivalent : JNAE (« Jump if Not Above Or Equal »)

  • JBE (« Jump if Below or Equal ») fait un saut au label spécifié si et seulement si CF = 1 ou ZF = 1. On l'utilise en arithmétique non signée pour savoir si un nombre est inférieur ou égal à un autre.

Mnémonique équivalent : JNA (« Jump if Not Above »)

IV-C-6-a-i. les sauts de test sur les flags

Ces instructions testent un flag unique et exécutent ou non le saut selon la valeur de ce flag.

  • JC (« Jump if Carry ») fait un saut au label spécifié si et seulement si CF = 1.

Remarques : Ce mnémonique correspond au même opcode que JB. Il est souvent employé pour vérifier que l'appel d'une interruption n'a pas déclenché d'erreur.

Exemple :

Image non disponible

Si l'appel de la fonction 3Eh de l'interruption 21H se solde par une erreur, alors la CF vaudra 1 et le saut sera accompli. Dans le cas opposé, l'exécution continuera normalement de manière linéaire.

  • JNC (« Jump if not Carry ») fait un saut au label spécifié si et seulement si CF = 0.
  • JZ (« Jump if Zero ») fait un saut au label spécifié si et seulement si ZF = 1. Ce mnémonique correspond au même opcode que JE.
  • JNZ (« Jump if not Zero ») fait un saut au label spécifié si et seulement si ZF = 0. Ce mnémonique correspond au même opcode que JNE.
  • JS (« Jump if Sign ») fait un saut au label spécifié si et seulement si SF = 1.
  • JNS (« Jump if not Sign ») fait un saut au label spécifié si et seulement si SF = 0.
  • JO (« Jump if Overflow ») fait un saut au label spécifié si et seulement si OF = 1.
  • JNO (« Jump if not Overflow ») fait un saut au label spécifié si et seulement si OF = 0.
  • JP (« Jump if Parity ») fait un saut au label spécifié si et seulement si PF = 1.
  • JNP (« Jump if not Parity ») fait un saut au label spécifié si et seulement si PF = 0.
IV-C-6-a-ii. le saut de test sur le registre CX

JCXZ (« Jump if CX = Zero ») fait un saut au label spécifié si et seulement si CX = 0.

IV-C-6-b. Les instructions arithmétiques

IV-C-6-b-i. L'instruction INC (« Increment »)

Syntaxe : INC Destination

Description : Incrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

Exemple : INC CL

IV-C-6-b-ii. L'instruction ADD (« Addition »)

Syntaxe : ADD Destination , Source

Description : Ajoute Source à Destination.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : ADD byte ptr [ VARIABLE + DI], 5

IV-C-6-b-iii. L'instruction ADC (« Add with Carry »)

Syntaxe : ADC Destination , Source

Description : Ajoute ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-iv. L'instruction DEC (« Decrement »)

Syntaxe : DEC Destination

Description : Décrémente Destination .

Indicateurs affectés : AF, OF, PF, SF, ZF

IV-C-6-b-v. L'instruction SUB (« Substract »)

Syntaxe : SUB Destination , Source

Description : Soustrait Source à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vi. L'instruction SBB (« Substract with Borrow »)

Syntaxe : SBB Destination , Source

Description : Soustrait ( Source + CF) à Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-vii. L'instruction MUL (« Multiply »)

Syntaxe : MUL Source

Description : Effectue une multiplication d'entiers non signés .

  • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
  • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
  • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

Indicateurs affectés : CF, OF

Remarque : Source ne peut être une valeur immédiate.

Exemples : MUL CX

MUL byte ptr [ TOTO ]

IV-C-6-b-viii. L'instruction IMUL (« Integer Multiply »)

Syntaxe : IMUL Source

IMUL Destination , Source

IMUL Destination , Source , Valeur

Description : Effectue une multiplication d'entiers signés .

IMUL Source :

  • Si Source est un octet : AL est multiplié par Source et le résultat est placé dans AX.
  • Si Source est un mot : AX est multiplié par Source et le résultat est placé dans DX:AX.
  • Si Source est un double-mot : EAX est multiplié par Source et le résultat est placé dans EDX:EAX.

IMUL Destination , Source : Multiplie Destination par Source et place le résultat dans Destination .

IMUL Destination , Source , Valeur : Multiplie Source par Valeur et place le résultat dans Destination .

Indicateurs affectés : CF, OF

IV-C-6-b-ix. L'instruction DIV (« Divide »)

Syntaxe : DIV Source

Description : Effectue une division euclidienne d'entiers non signés .

  • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
  • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
  • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

Indicateurs affectés : AF, OF, PF, SF, ZF

Remarque : Source ne peut être une valeur immédiate.

IV-C-6-b-x. L'instruction IDIV (« Integer Divide »)

Syntaxe : IDIV Source

Description : Effectue une division euclidienne d'entiers signés .

  • Si Source est un octet : AX est divisé par Source , le quotient est placé dans AL et le reste dans AH.
  • Si Source est un mot : DX:AX est divisé par Source , le quotient est placé dans AX et le reste dans DX.
  • Si Source est un double-mot : EDX:EAX est divisé par Source , le quotient est placé dans EAX et le reste dans EDX.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-b-xi. L'instruction NEG (« Negation »)

Syntaxe : NEG Destination

Description : Forme le complément à 2 de Destination , i.e. prend l'opposé de Destination .

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Remarque : À ne pas confondre avec NOT, qui forme le complément à 1.

IV-C-6-c. Les instructions logiques

IV-C-6-c-i. L'instruction NOT (« Logical NOT »)

Syntaxe : NOT Destination

Description : Effectue un NON logique bit à bit sur Destination (i.e. chaque bit de Destination est inversé).

IV-C-6-c-ii. L'instruction OR (« Logical OR »)

Syntaxe : OR Destination , Source

Description : Effectue un OU logique inclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Afin d'optimiser la taille et les performances du programme, on peut utiliser l'instruction “OR AX, AX” à la place de “CMP AX, 0”.
En effet, un OU bit à bit entre deux nombres identiques ne modifie pas Destination et est exécuté « infiniment » plus rapidement qu'une soustraction. Comme les flags sont affectés, les sauts conditionnels sont possibles.

IV-C-6-c-iii. L'instruction AND (« Logical AND »)

Syntaxe : AND Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

IV-C-6-c-iv. L'instruction TEST (« Test for bit pattern »)

Syntaxe : TEST Destination , Source

Description : Effectue un ET logique bit à bit entre Destination et Source . Le résultat n'est pas conservé, donc Destination n'est pas modifié. Seuls les flags sont affectés.

Cet opérateur est souvent utilisé pour tester certains bits de Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Exemple :

IV-C-6-c-v. L'instruction XOR (« Exclusive logical OR »)

Syntaxe : XOR Destination , Source

Description : Effectue un OU logique exclusif bit à bit entre Destination et Source . Le résultat est stocké dans Destination .

Indicateurs affectés : CF, OF, PF, SF, ZF

Remarque : Pour remettre un registre à zéro, il est préférable de faire “XOR AX, AX” que “MOV AX, 0”. En effet, le résultat est le même, mais la taille et surtout la vitesse d'exécution de l'instruction sont très largement optimisées.

IV-C-6-c-vi. L'instruction SHL (« Shift logical Left »)

Syntaxe : SHL Destination , Source

Description : Décale les bits de Destination de Source positions vers la gauche. Les bits les plus à droite sont remplacés par des zéros.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Exemple : SHL AX, 4 ;permet de multiplier par 16 de façon infiniment plus rapide que MUL

Mnémonique équivalent : SAL (« Shift Arithmetical Left »)

IV-C-6-c-vii. L'instruction SHR (« Shift logical Right »)

Syntaxe : SHR Destination , Source

Description : Décale les bits de Destination de Source positions vers la droite. Les bits les plus à gauche sont remplacés par des zéros.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

Mnémonique équivalent : SAR (« Shift Arithmetical Right »)

IV-C-6-c-viii. L'instruction ROL (« Rotate Left »)

Syntaxe : ROL Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. Le dernier bit à être sorti à gauche et à être rentré à droite est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-ix. L'instruction ROR (« Rotate Right »)

Syntaxe : ROR Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la droite. Le dernier bit à être sorti à droite et à être rentré à gauche est placé dans CF. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-x. L'instruction RCL (« Rotate through Carry Left »)

Syntaxe : RCL Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la gauche. CF est utilisé comme intermédiaire : chaque bit qui sort à gauche est placé dans CF, et le contenu de CF est ensuite réinséré à droite. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-c-xi. L'instruction RCR (« Rotate through Carry Right »)

Syntaxe : RCR Destination , Source

Description : Effectue une rotation des bits de Destination de Source positions vers la droite. CF est utilisé comme intermédiaire : chaque bit qui sort à droite est placé dans CF, et le contenu de CF est ensuite réinséré à gauche. OF est mis à 1 si et seulement si le signe de Destination a changé.

Indicateurs affectés : AF, CF, OF, PF, SF, ZF

IV-C-6-d. Les instructions de manipulation des flags

IV-C-6-d-i. L'instruction CLC (« Clear Carry flag »)

Syntaxe : CLC

Description : Met CF à 0.

Indicateurs affectés : CF

IV-C-6-d-ii. L'instruction STC (« Set Carry flag »)

Syntaxe : STC

Description : Met CF à 1.

Indicateurs affectés : CF

IV-C-6-d-iii. L'instruction CLD (« Clear Direction flag »)

Syntaxe : CLD

Description : Met DF à 0.

Indicateurs affectés : DF

IV-C-6-d-iv. L'instruction STD (« Set Direction flag »)

Syntaxe : STD

Description : Met DF à 1.

Indicateurs affectés : DF

IV-C-6-d-v. L'instruction CLI (« Clear Interrupt flag »)

Syntaxe : CLI

Description : Met IF à 0.

Indicateurs affectés : IF

IV-C-6-d-vi. L'instruction STI (« Set Interrupt flag »)

Syntaxe : STI

Description : Met IF à 1.

Indicateurs affectés : IF

IV-C-6-d-vii. L'instruction CMC (« Complement Carry flag »)

Syntaxe : CMC

Description : Inverse CF.

Indicateurs affectés : CF

IV-C-6-d-viii. L'instruction LAHF (« Load AH from Flags »)

Syntaxe : LAHF

Description : Charge dans AH l'octet de poids faible du registre des indicateurs.

IV-C-6-d-ix. L'instruction SAHF (« Store AH into Flags »)

Syntaxe : SAHF

Description : Stocke les bits de AH dans le registre des indicateurs.

IV-C-6-e. Les instructions de gestion de la pile

IV-C-6-e-i. L'instruction PUSH (« Push Word onto Stack »)

Syntaxe : PUSH Source

Description : Empile le mot Source. SP est décrémenté de 2.

Remarques : Source ne peut être une valeur immédiate. Il est possible d'abréger votre code source en écrivant par exemple “PUSH AX BX BP”. Le compilateur écrira alors trois fois l'instruction PUSH du langage machine. Il est possible également d'empiler des doubles-mots.

IV-C-6-e-ii. L'instruction POP (« Pop Word off Stack »)

Syntaxe : POP Destination

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans Destination. SP est incrémenté de 2.

IV-C-6-e-iii. L'instruction PUSHF (« Push Flags onto Stack »)

Syntaxe : PUSHF

Description : Empile le registre des indicateurs. SP est décrémenté de 2.

Remarque : PUSHFD empile le registre des indicateurs codé sur 32 bits.

IV-C-6-e-iv. L'instruction POPF (« Pop Flags off Stack »)

Syntaxe : POPF

Description : Dépile le mot qui se trouve au sommet de la pile et le place dans le registre des indicateurs. SP est incrémenté de 2.

Indicateurs affectés : Tous

Remarque : POPFD est utilisé pour un registre des indicateurs codé sur 32 bits.

IV-C-6-e-v. L'instruction PUSHA (« Push All registers onto Stack »)

Syntaxe : PUSHA

Description : Empile AX, BX, CX, DX, BP, SI, DI et SP.

Remarque : PUSHAD est utilisé pour des registres de 32 bits.

IV-C-6-e-vi. L'instruction POPA (« Pop All registers off Stack »)

Syntaxe : POPA

Description : Restaure AX, BX, CX, DX, BP, SI, DI et SP à partir de la pile.

Remarque : POPAD est utilisé pour des registres de 32 bits.

IV-C-6-f. Les instructions de gestion des chaînes d'octets

IV-C-6-f-i. L'instruction MOVSB (« Move String Byte »)

Syntaxe : MOVSB

Description : Copie l'octet adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Pour copier plusieurs octets, faire REP MOVSB (« Repeat Move String Byte »). Le nombre d'octets à copier doit être transmis dans CX de même que pour un LOOP.

Exemple :

Image non disponible

IV-C-6-f-ii. L'instruction SCASB (« Scan String Byte »)

Syntaxe : SCASB

Description : Compare l'octet adressé par ES:DI avec AL. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarques : Pour comparer plusieurs octets, faire “REP SCASB” ou “REPE SCASB” (« Repeat until Egal »), ou encore “REPZ SCASB” (« Repeat until Zero »). Ces trois préfixes sont équivalents.
Le nombre d'octets à comparer doit être transmis dans CX. La boucle ainsi créée s'arrête si CX = 0 ou si le caractère pointé par ES:DI est le même que celui contenu dans AL (i.e. si ZF = 1). On peut ainsi rechercher un caractère dans une chaîne.
Pour répéter au contraire la comparaison jusqu'à ce que ZF = 0, c'est-à-dire jusqu'à ce que AL et le caractère adressé par ES:DI diffèrent, utiliser REPNE ou REPNZ.

Exemple :

Image non disponible

IV-C-6-f-iii. L'instruction LODSB (« Load String Byte »)

Syntaxe : LODSB

Description : Charge dans AL l'octet adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour MOVSB.

Syntaxe : STOSB

Description : Stocke le contenu de AL dans l'octet adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté, sinon il est décrémenté.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

Syntaxe : CMPSB

Description : Compare l'octet adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés, sinon ils sont décrémentés.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-g. Les instructions de gestion des chaînes de mots

Ce sont les mêmes que les précédentes, hormis qu'elles traitent des mots et non des octets et que leur nom se termine par un ‘W' au lieu du ‘B'.

IV-C-6-g-i. L'instruction MOVSW (« Move String Word »)

Syntaxe : MOVSW

Description : Copie le mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Pour copier plusieurs mots, faire “REP MOVSW”. Le nombre de mots à copier doit être transmis dans CX.

IV-C-6-g-ii. L'instruction SCASW (« Scan String Word »)

Syntaxe : SCASW

Description : Compare le mot adressé par ES:DI avec AX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de m0ême que pour SCASB.

IV-C-6-g-iii. L'instruction LODSW (« Load String Word »)

Syntaxe : LODSW

Description : Charge dans AX le mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-g-iv. L'instruction STOSW (« Store String Word »)

Syntaxe : STOSW

Description : Stocke le contenu de AX dans le mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 2, sinon il est décrémenté de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-g-v. L'instruction CMPSW (« Compare String Word »)

Syntaxe : CMPSW

Description : Compare le mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 2, sinon ils sont décrémentés de 2.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-h. Les instructions de gestion des chaînes de doubles-mots

Elles traitent des doubles-mots et leur nom se termine par un ‘D'.

IV-C-6-h-i. L'instruction MOVSD (« Move String Double Word »)

Syntaxe : MOVSD

Description : Copie le double-mot adressé par DS:SI à l'adresse ES:DI. Si DF = 0, alors DI et SI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Pour copier plusieurs doubles-mots, faire “REP MOVSD”. Le nombre de doubles-mots à copier doit être transmis dans CX.

IV-C-6-h-ii. L'instruction SCASD (« Scan String Double Word »)

Syntaxe : SCASD

Description : Compare le double-mot adressé par ES:DI avec EAX. Les résultats sont placés dans le registre des indicateurs. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour SCASB.

IV-C-6-h-iii. L'instruction LODSD (« Load String Double Word »)

Syntaxe : LODSD

Description : Charge dans EAX le double-mot adressé par DS:SI. Si DF = 0, alors SI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour LODSB.

IV-C-6-h-iv. L'instruction STOSD (« Store String Double Word »)

Syntaxe : STOSD

Description : Stocke le contenu de EAX dans le double-mot adressé par ES:DI. Si DF = 0, alors DI est ensuite incrémenté de 4, sinon il est décrémenté de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour STOSB.

IV-C-6-h-v. L'instruction CMPSD (« Compare String Double Word »)

Syntaxe : CMPSD

Description : Compare le double-mot adressé par DS:SI et celui adressé par ES:DI. Si DF = 0, alors SI et DI sont ensuite incrémentés de 4, sinon ils sont décrémentés de 4.

Remarque : Possibilité d'utiliser les préfixes de répétition, de même que pour CMPSB.

IV-C-6-i. Les instructions d'appel

IV-C-6-i-i. L'instruction CALL (« Procedure Call »)

Syntaxe : CALL MaProc

Description : Appel de procédure. Si MaProc se trouve dans un segment extérieur, le processeur empile CS. Ensuite, dans tous les cas, il empile IP et fait un saut à l'étiquette MaProc.

IV-C-6-i-ii. L'instruction RET (ou RETN)

Syntaxe : RET

Description : Retour de procédure se trouvant à l'intérieur du segment (NEAR). Un mot est dépilé et placé dans IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-i-iii. L'instruction RETF

Syntaxe : RETF

Description : Retour de procédure se trouvant à l'extérieur du segment (FAR). Deux mots sont dépilés et placés dans CS:IP. Le contrôle retourne donc à la procédure appelante.

IV-C-6-j. Les instructions de boucle

IV-C-6-j-i. L'instruction LOOP

Syntaxe : LOOP MonLabel

Description : Décrémente CX, puis, si CX <> 0, fait un saut à MonLabel.

IV-C-6-j-ii. L'instruction LOOPE (« Loop while Equal »)

Syntaxe : LOOPE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 1, fait un saut à MonLabel.

Mnémonique équivalent : LOOPZ

IV-C-6-j-iii. L'instruction LOOPNE (« Loop while not Equal »)

Syntaxe : LOOPNE MonLabel

Description : Décrémente CX, puis, si CX <> 0 et ZF = 0, fait un saut à MonLabel.

Mnémonique équivalent : LOOPNZ

IV-C-6-k. Les instructions d'adressage

IV-C-6-k-i. L'instruction LEA (« Load effective address »)

Syntaxe : LEA Destination, Source

Description : Charge l'offset de la source dans le registre Destination.

Exemple : LEA BP, word ptr [BP + TOTO]

IV-C-6-k-ii. L'instruction LDS (« Load pointer using DS »)

Syntaxe : LDS Destination, Source

Description : Transfère dans DS:Destination le contenu de la mémoire adressée par Source.

IV-C-6-k-iii. L'instruction LES (« Load pointer using ES »)

Syntaxe : LES Destination, Source

Description : Transfère dans ES:Destination le contenu de la mémoire adressée par Source.

IV-C-6-l. Les instructions de conversion arithmétique

Remarque préliminaire : Nous n'expliciterons pas toutes ces instructions.

IV-C-6-l-i. L'instruction AAA (« ASCII Adjust for Addition »)
IV-C-6-l-ii. L'instruction AAD (« ASCII Adjust for Division »)
IV-C-6-l-iii. L'instruction AAM (« ASCII Adjust for Multiplication »)
IV-C-6-l-iv. L'instruction AAS (« ASCII Adjust for Substraction »)
IV-C-6-l-v. L'instruction CBW (« Convert Byte to Word »)

Syntaxe : CBW

Description : Convertit l'octet signé stocké dans AL en un mot (signé) stocké dans AX. Ainsi, si AL est négatif, AH sera rempli de 1 binaires, sinon, AH sera mis à 0.

IV-C-6-l-vi. L'instruction CWD (« Convert Word to Double Word »)

Syntaxe : CWD

Description : Convertit le mot signé stocké dans AX en un double-mot (signé) stocké dans DX:AX. Ainsi, si AX est négatif, DX sera rempli de 1 binaires, sinon DX sera mis à 0.

IV-C-6-l-vii. L'instruction DAA (« Decimal Adjust for Addition »)
IV-C-6-l-viii. L'instruction DAS (« Decimal Adjust for Substraction »)
IV-C-6-l-ix. L'instruction MOVSX (« Move with Sign Extend »)

Syntaxe : MOVSX Destination, Source

Description : Déplace le contenu signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. Si Source est négatif, la partie haute de Destination sera remplie de 1 binaires, sinon elle sera remplie de 0.

IV-C-6-l-x. L'instruction MOVZX (« Move with Zero Extend »)

Syntaxe : MOVZX Destination, Source

Description : Déplace le contenu non signé d'un registre de 8 bits dans un registre de 16 bits, ou bien déplace le contenu signé d'un registre de 16 bits dans un registre de 32 bits. La partie haute de Destination sera donc mise à 0.

IV-C-6-l-xi. L'instruction XLAT (« Translate »)

IV-C-6-m. Les instructions d'entrée-sortie

IV-C-6-m-i. L'instruction IN (« Input from port »)

Syntaxe : IN Destination, Port

Description : Charge un octet ou un mot depuis un port d'entrée-sortie dans AL ou AX. Port peut être DX ou bien une constante de 8 bits.

IV-C-6-m-ii. L'instruction OUT (« Output to port »)

Syntaxe : OUT Port, Source

Description : Écrit dans Port la valeur contenue dans Source. Source ne peut être que AL ou AX. Port est une constante ou bien DX.

V. QUATRIÈME PARTIE

Remarque préliminaire : Cette partie présente quelques-unes des interruptions du DOS qui servent à manier les fichiers d'un disque. Nous la conclurons par l'étude d'un exemple de programme qui crypte un fichier choisi par l'utilisateur.

V-A. LECTURE ET ÉCRITURE DE FICHIERS AVEC LES HANDLES

Sous DOS, il existe deux méthodes pour accéder aux fichiers et pour chacune d'elles un lot d'interruptions spécifiques. La première est la méthode des FCB (« File contol block »). Elle est rarement utilisée, aussi ne l'aborderons-nous pas. Nous étudierons le principe de la méthode des « handles ».

Pour lire ou écrire des données dans un fichier, il est nécessaire de l'ouvrir, c'est-à-dire de le charger en mémoire. Quand toutes les opérations de lecture et d'écriture auront été effectuées, le fichier devra être refermé afin d'enregistrer les éventuelles dernières modifications et surtout de libérer la mémoire occupée.

V-A-1. Ouverture d'un fichier

On ouvre un fichier en appelant la fonction 3dh de l'interruption 21h. Celle-ci attend comme paramètre dans DS:DX l'adresse d'une chaîne de caractères qui contient le chemin d'accès au fichier sur un disque, par exemple “C:\MonDoss\MonFic.txt”.

Remarque : il n'est pas indispensable de mentionner le chemin d'accès complet : par défaut, le fichier sera cherché à partir du dossier courant.

Remarque importante : La chaîne doit être impérativement suivie de l'octet 00h qui sert à marquer sa fin.

Il nous faut également spécifier le mode d'accès en écrivant dans AL un 0 (si on veut ouvrir le fichier en lecture seule), un 1 (si on veut l'ouvrir en écriture seule) ou un 2 (lecture ET écriture).

Si l'interruption échoue, le flag CF sera mis à 1 sans que le fichier soit ouvert. Dans le cas contraire, CF est mis 0 et le registre AX contient un petit nombre entier (par exemple 5) appelé « handle » (ce qui signifie « poignée ») du fichier. Ce handle représente le fichier. C'est lui qu'il faudra désormais invoquer pour effectuer des opérations de lecture ou d'écriture, et non pas le chemin d'accès.
En effet, les chemins d'accès ne sont donc plus d'aucune utilité puisque les fichiers sont ouverts dans la mémoire vive.

V-A-2. Lecture dans un fichier

Une fois le fichier ouvert, on peut le lire avec la fonction 3fh. Il suffit de mentionner le handle dans BX, le nombre d'octets à lire dans CX, et l'adresse d'un buffer dans DS:DX.
Au cas où vous ne sauriez pas ce qu'est un buffer (ou tampon), sachez que c'est simplement une variable (généralement une chaîne de caractères) destinée à recevoir des données (ou à en fournir). Dans notre cas, le buffer va recevoir les octets lus dans le fichier.

Après l'appel, AX contient le nombre d'octets qui ont été effectivement lus (il peut être inférieur à la taille demandée si le fichier n'est pas assez long). En cas de problème, CF sera mis à 1.

V-A-3. Écriture dans un fichier

Pour écrire des données, on procède de même avec la fonction 40h. Les paramètres sont les mêmes que pour la fonction 3fh. Le buffer contient cette fois les octets à écrire. Après l'appel, le nombre d'octets qui ont été effectivement écrits est stocké dans AX (il sera être inférieur à la taille spécifiée si le disque est plein).

Les données sont écrites sur le disque dur que le tampon (dans la mémoire vive) est plein.

V-A-4. Existence d'un pointeur de fichier

Une question se pose cependant : à quel endroit du fichier les données sont-elles lues (ou écrites) ?

Réponse : quand un fichier est ouvert, un pointeur spécial pointant vers le début du fichier est créé. La première opération de lecture (ou d'écriture) se fera donc au début du fichier.
Mais entre chaque opération, le pointeur est incrémenté de la taille des données que l'on a lues (ou écrites). La deuxième opération se fera donc sur les octets qui suivent ceux de la première.

Remarque : Il est possible de modifier directement le pointeur de fichier : voyez pour cela la fonction 42h…

V-A-5. Fermeture d'un fichier

Pour terminer, le fichier doit être refermé. Les modifications éventuellement apportées et non enregistrées seront écrites sur le disque, et le handle sera libéré. C'est la fonction 3eh qui se charge de tout cela. Elle attend simplement le handle du fichier dans BX. Et comme d'habitude, CF vaut 1 après l'appel si des erreurs ont été rencontrées.

Remarque : Attention lorsque vous laissez le fichier ouvert longtemps afin d'y ajouter progressivement des données ! Si le système plante, vous perdrez les données qui se trouvent dans le tampon à ce moment. C'est pourquoi il est conseillé de forcer régulièrement l'écriture sur le disque en refermant le fichier.

V-A-6. Conclusion

Le tableau suivant récapitule ces différentes étapes :

Fonction | Description | Paramètres
3d h | Ouvrir le fichier | - DS:DX : adresse d'une chaîne contenant le chemin d'accès - AL : mode d'accès
3e h | Fermer le fichier | - BX : handle
3f h | Lire le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer
40 h | Écrire dans le fichier | - BX : handle - CX : nombre d'octets - DS:DX : adresse d'un buffer

V-B. LES FONCTIONS DE RECHERCHE DE FICHIERS

Pour rechercher un fichier (ou un dossier), on se sert des fonctions 4eh (« Find First ») et 4fh (« Find Next »).

Imaginons par exemple que nous voulions chercher dans le dossier courant tous les fichiers qui portent l'extension “ .com ” afin de les supprimer. Comment devons-nous nous y prendre ?

V-B-1. La fonction 4eh

La fonction 4eh sert à définir des critères de recherche et à trouver le premier fichier qui correspond à ces critères (s'il existe).

On doit passer dans DS:DX l'adresse de la chaîne de caractères qui contient le masque de recherche (dans notre exemple, ce masque est “ *.com ”). Par défaut, les fichiers sont cherchés dans le dossier courant. Mais on peut évidemment spécifier un autre chemin dans le masque.

Remarque importante : afin que le DOS puisse connaître sa taille, le masque doit impérativement être terminé par l'octet 00h !

On écrit également dans CX les attributs des fichiers que l'on désire trouver. Si CX vaut 0, seuls les fichiers « normaux » pourront être trouvés. En fait, chaque bit de CL représente un attribut, comme le montre le tableau ci-dessous :

Bit | Signification
1 | Lecture seule
2 | Fichier caché
3 | Fichier système
4 | Volume
5 | Répertoire
6 | Fichier
7 | (Aucune…)
8 | (Aucune…)

Pour demander à la fonction 4eh de ne pas oublier les fichiers cachés, il suffit donc de charger CX avec la valeur 2 (bit numéro 2 = 1). De même, l'attribut 00000111 b (soit 7) nous permettra de trouver les fichiers en lecture seule, les fichiers cachés et les fichiers systèmes.

Remarque : Ne vous souciez pas trop des bits numéro 4 et 6. Laissez-les à 0.

Une fois que les paramètres ont été ajustés, on peut appeler la fonction 4eh. Si aucun fichier n'a été trouvé, le flag CF est mis à 1 . On doit donc faire un test sur CF pour savoir si la recherche peut continuer ou si elle doit s'arrêter.
Si au contraire la fonction a trouvé un fichier, les caractéristiques de ce fichier (i.e. son nom, sa taille, ses attributs…) sont inscrites dans une zone de la mémoire appelée DTA (« Disk Transfer Area »).

Mais où se trouve donc cette DTA et à quoi ressemble-t-elle ?

Réponse : par défaut, le DOS place la DTA dans le PSP de votre programme, à l'offset 80h.

Remarque : il vous est naturellement possible de la déplacer en faisant appel à la fonction 1ah.

Voici la structure de la DTA :

Adresse | Description | Taille (octets)
00 h | Lettre du lecteur (0=courant, 1=A, 2=B…) sur lequel se trouve le fichier | 1
01 h | Modèle de la recherche | 11
0C h | Réservé | 9
15 h | Attributs du fichier | 1
16 h | Heure du fichier | 2
18 h | Date du fichier | 2
1A h | Taille du fichier | 4
1E h | Nom du fichier avec l'extension | 13

Puisque par défaut la DTA est située dans le PSP à l'offset 80h, le nom du fichier trouvé est écrit à l'offset 80h + 1eh = 9eh. De même, la taille se trouve à l'offset 9ah, etc.

V-B-2. La fonction 4fh

Jusqu'à présent, nous n'avons trouvé qu'un seul fichier ! Pour poursuivre la recherche, appelez la fonction 4fh sans écrire aucun paramètre dans les registres. Les caractéristiques de votre recherche ont été mémorisées dans la DTA : vous n'avez donc pas à les rappeler. De même que pour la fonction 4eh, CF est mis à 1 si aucun nouveau fichier n'a été trouvé. C'est le signe que vous pouvez arrêter votre recherche.

V-B-3. Conclusion

Résumons-nous :

Fonction | Description | Paramètres
4e h | Trouver le premier fichier qui correspond aux caractéristiques spécifiées | - DS:DX : adresse d'une chaîne contenant le masque - CX : attributs
4f h | Trouver le prochain fichier | Aucun !

À titre d'exemple, écrivons à présent le programme que nous évoquions tout à l'heure : “ trouver tous les fichiers COM du dossier courant et les effacer ”.

V-B-4. EXEMPLE DE PROGRAMME

V-B-5. Entrée bufferisée au clavier

L'exemple que nous proposons ici fait appel à une saisie au clavier. Comme nous n'en avons pas encore rencontré, ce paragraphe explique brièvement comment utiliser les fonctions 0ah et 0ch.

On peut se servir de la fonction 0ah pour lire une chaîne de caractères au clavier. Le seul paramètre à fournir est l'adresse d'un buffer. Comme il est souvent indispensable d'effacer le buffer avant la lecture, il est préférable de ne pas utiliser la fonction 0ah directement, mais d'appeler la fonction 0ch. En effet, celle-ci commence par effacer le buffer, puis appelle la fonction 0ah (ou une fonction voisine dont le numéro doit être transmis dans AL).

Voici le code à écrire :

Image non disponible

La seule difficulté est dans la déclaration du buffer. Voici comment vous devez vous y prendre si vous considérez que l'utilisateur pourra entrer au plus n caractères :

Image non disponible

Le premier octet de “maChaine” doit contenir le nombre d'octets maximal qui pourront être entrés, plus 1. Pourquoi plus 1 ? Tout simplement parce que le DOS met à la fin des caractères tapés le code ASCII 13 (retour chariot).

Pour comprendre la suite de la déclaration, il faut savoir de quelle manière le DOS transmet les caractères qui ont été lus. Après la lecture, le deuxième octet du buffer contiendra le nombre exact d'octets lus (sans compter le 13 final). La chaîne proprement dite ne commencera donc qu'au troisième octet. Puisqu'un octet est utilisé pour donner au DOS le nombre maximal de caractères autorisés, un autre pour recevoir le nombre de caractères lus effectivement, et encore un autre pour recevoir le code ASCII 13, le buffer doit comporter trois octets de plus que la taille maximale de la chaîne. C'est pourquoi on écrit :

maChaine db n+1, ?, n dup(?), ?

V-B-6. Le programme

Le programme suivant demande à l'utilisateur d'entrer le nom d'un fichier se trouvant dans le dossier courant puis crypte ce fichier en appliquant un NOT logique sur chaque octet. Naturellement, il n'est pas optimisé du tout. Ce n'est qu'un exemple !

Image non disponible

Suggestion : quand vous aurez compris ce petit programme, essayez donc de l'améliorer ! Par exemple, il faudrait que l'utilisateur puisse taper le nom du fichier à crypter directement comme paramètre du programme (i.e. “CRY MONFICH.EXE”). Voyez pour cela la structure du PSP telle que présentée dans la première partie. De plus, le programme devrait pouvoir trouver automatiquement le nom du fichier d'arrivée : ce serait le même que celui du fichier à crypter, mais avec une extension différente (par exemple “.cry”).
Vous pourriez également étendre les possibilités du logiciel en permettant à l'utilisateur de saisir non plus un simple nom de fichier, mais un masque complexe avec des chemins d'accès et des caractères “jokers” comme '*' et '?'. Vous aurez donc besoin des fonctions de recherche 4eh et 4fh. L'utilisateur doit de surcroît pouvoir choisir s'il veut crypter un fichier ou bien le décrypter…

VI. CONCLUSION

Voilà. Nous espérons que vous avez compris l'essentiel de ce cours, à savoir la logique de la programmation en assembleur. Les connaissances que vous avez acquises en lisant la fin du cours devraient vous permettre de mieux saisir les premiers chapitres. C'est pourquoi nous vous invitons à relire ce tutoriel.

Certaines notions doivent encore vous paraître obscures. Maintenant que vous vous êtes familiarisé avec l'assembleur, commencez par écrire et tester quelques petits programmes COM inutiles et très simples. Vous verrez qu'au bout de quelques essais, tout ce que vous avez lu vous semblera très concret.

Lorsque vous aurez écrit vous-même quelques tout petits programmes qui fonctionnent, l'assimilation du langage deviendra alors très rapide, car l'assembleur est un langage très logique et très cohérent.

Nous vous conseillons d'écrire une bibliothèque de petites macros ou procédures qui vous serviront dans tous vos programmes. Par exemple, vous pouvez faire une macro qui affiche un entier à l'écran, ou bien qui convertit une chaîne de caractères en un nombre entier.

Voici un exemple de macro qui renvoie dans AL le nombre de chiffres d'un entier non signé de deux octets passé dans AX :

Image non disponible

N.B. Pensez avant toute chose à vous munir d'une liste des interruptions du DOS. Vous ne pourrez rien faire si vous n'en avez pas !


Langage Assembleur PC

Langage Assembleur PC Les deux auteurs ... Paul A. Carter Sébastien Le Ray
Table des matières Developpez.com Copyright © 2000-2014 - www.developpez.com

  • I. Préface
    • I-A. Objectif
    • I-B. Remerciements
    • I-C. Ressources sur Internet
    • I-D. Réactions
  • II. Introduction
    • II-A. Systèmes numériques
      • II-A-1. Décimal
      • II-A-2. Binaire
      • II-A-3. Hexadécimal
    • II-B. Organisation de l'ordinateur
      • II-B-1. Mémoire
      • II-B-2. Le CPU (processeur)
      • II-B-3. La famille des processeurs 80x86
      • II-B-4. Regitres 16 bits du 8086
      • II-B-5. Registres 32 bits du 80386
      • II-B-6. Mode réel
      • II-B-7. Mode protégé 16 bits
      • II-B-8. Mode protégé 32 bits
      • II-B-9. Interruptions
    • II-C. Langage assembleur
      • II-C-1. Langage machine
      • II-C-2. Langage d'assembleur
      • II-C-3. Opérandes d'instruction
      • II-C-4. Instructions de base
      • II-C-5. Directives
        • II-C-5-a. La directive equ
        • II-C-5-b. La directive ?ne
        • II-C-5-c. Directives de données
      • II-C-6. Entrées et sorties
      • II-C-7. Débogage
    • II-D. Créer un programme
      • II-D-1. Premier programme
      • II-D-2. Dépendance vis-à-vis du compilateur
      • II-D-3. Assembler le code
      • II-D-4. Compiler le code C
      • II-D-5. Lier les fichiers objet
      • II-D-6. Comprendre un listing assembleur
    • II-E. Fichier squelette
  • III. Bases du langage assembleur
    • III-A. Travailler avec les entiers
      • III-A-1. Représentation des entiers
        • III-A-1-a. Grandeur signée
        • III-A-1-b. Complément à 1
        • III-A-1-c. Complément à 2
      • III-A-2. Extension de signe
        • III-A-2-a. Réduire la taille des données
        • III-A-2-b. Augmenter la taille des données
        • III-A-2-c. Application à la programmation en C
      • III-A-3. Arithmétique en complément à 2
      • III-A-4. Programme exemple
      • III-A-5. Arithmétique en précision étendue
    • III-B. Structures de contrôle
      • III-B-1. Comparaison
      • III-B-2. Instructions de branchement
      • III-B-3. Les instructions de boucle
    • III-C. Traduire les structures de contrôle standard
      • III-C-1. Instructions if
      • III-C-2. Boucles while
      • III-C-3. Boucles do while
    • III-D. Exemple : trouver des nombres premiers
  • IV. Opérations sur les bits
    • IV-A. Opérations de décalage
      • IV-A-1. Décalages logiques
      • IV-A-2. Utilisation des décalages
      • IV-A-3. Décalages arithmétiques
      • IV-A-4. Décalages circulaires
      • IV-A-5. Application simple
    • IV-B. Opérations booléennes niveau bit
      • IV-B-1. L'opération ET
      • IV-B-2. L'opération OU
      • IV-B-3. L'opération XOR
      • IV-B-4. L'opération NOT
      • IV-B-5. L'instruction TEST
      • IV-B-6. Utilisation des opérations sur les bits
    • IV-C. Éviter les branchements conditionnels
    • IV-D. Manipuler les bits en C
      • IV-D-1. Les opérateurs niveau bit du C
      • IV-D-2. Utiliser les opérateurs niveau bit en C
    • IV-E. Représentations big et little endian
      • IV-E-1. Quand se soucier du caractère big ou little endian
    • IV-F. Compter les bits
      • IV-F-1. Méthode une
      • IV-F-2. Méthode deux
      • IV-F-3. Méthode trois
  • V. Sous-programmes
    • V-A. Adressage indirect
    • V-B. Exemple de sous-programme simple
    • V-C. La pile
    • V-D. Les Instructions CALL et RET
    • V-E. Conventions d'appel
      • V-E-1. Passer les paramètres via la pile
      • V-E-2. Variables locales sur la pile
    • V-F. Programme multimodule
    • V-G. Interfacer de l'assembleur avec du C
      • V-G-1. Sauvegarder les registres
      • V-G-2. Étiquettes de fonctions
      • V-G-3. Passer des paramètres
      • V-G-4. Calculer les adresses des variables locales
      • V-G-5. Retourner des valeurs
      • V-G-6. Autres conventions d'appel
      • V-G-7. Exemples
      • V-G-8. Appeler des fonctions C depuis l'assembleur
    • V-H. Sous-programmes réentrants et récursifs
      • V-H-1. Sous-programmes récursifs
      • V-H-2. Révision des types de stockage des variables en C
  • VI. Tableaux
    • VI-A. Introduction
      • VI-A-1. Définir des tableaux
        • VI-A-1-a. Définir des tableaux dans les segments data et bss
        • VI-A-1-b. Définir des tableaux comme variables locales
      • VI-A-2. Accéder aux éléments de tableaux
      • VI-A-3. Adressage indirect plus avancé
      • VI-A-4. Exemple
        • VI-A-4-a. L'instruction LEA revisitée
      • VI-A-5. Tableaux Multidimensionnels
        • VI-A-5-a. Tableaux à Deux Dimensions
        • VI-A-5-b. Dimensions supérieures à deux
        • VI-A-5-c. Passer des tableaux multidimensionnels comme paramètres en C
    • VI-B. Instructions de tableaux/chaînes
      • VI-B-1. Lire et écrire en mémoire
      • 0VI-B-2. Le préfixe d'instruction REP
      • VI-B-3. Instructions de comparaison de chaînes
      • VI-B-4. Les préfixes d'instruction REPx
      • VI-B-5. Exemple
  • VII. Virgule flottante
    • VII-A. Représentation en virgule flottante
      • VII-A-1. Nombres binaires non entiers
      • VII-A-2. Représentation en virgule flottante IEEE
        • VII-A-2-a. Simple précision IEEE
        • VII-A-2-b. Nombres dénormalisés
        • VII-A-2-c. Double précision IEEE
    • VII-B. Arithmétique en virgule flottante
      • VII-B-1. Addition
      • VII-B-2. Soustraction
      • VII-B-3. Multiplication et division
      • VII-B-4. Conséquences sur la programmation
    • VII-C. Le coprocesseur arithmétique
      • VII-C-1. Matériel
      • VII-C-2. Instructions
        • VII-C-2-a. Chargement et stockage
        • VII-C-2-b. Addition et soustraction
        • VII-C-2-c. Multiplication et division
        • VII-C-2-d. Comparaisons
        • VII-C-2-e. Instructions diverses
      • VII-C-3. Exemple : formule quadratique
      • VII-C-4. Exemple : lire un tableau depuis un fichier
      • VII-C-5. Exemple : rechercher les nombres premiers
  • VIII. Structures et C++
    • VIII-A. Structures
      • VIII-A-1. Introduction
      • VIII-A-2. Alignement en mémoire
      • VIII-A-3. Champs de bits
      • VIII-A-4. Utiliser des structures en assembleur
    • VIII-B. Assembleur et C++
      • VIII-B-1. Surcharge et décoration de noms
      • VIII-B-2. Références
      • VIII-B-3. Fonctions inline
      • VIII-B-4. Classes
        • VIII-B-4-a. Exemple
      • VIII-B-5. Héritage et polymorphisme
      • VIII-B-6. Autres fonctionnalités C++
  • IX. Annexe A : Instructions 80x86
    • IX-A. Instructions hors virgule flottante
    • IX-B. Instruction en virgule flottante
Langage Assembleur PC
Les deux auteurs ... Paul A. Carter Sébastien Le Ray

http://rd.himediads.com/fr/jump/developpez.com/
Publié le 20 mars 2005 - Mis à jour le 30 janvier 2013
http://rd.himediads.com/fr/ad/developpez.com/

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de
présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ©2013 Paul A. Carter. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse
de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique Assembleur : Alcatîz- Contacter par email
Developpez.com Copyright © 2000-2014 - www.developpez.com

Copyright © 2001, 2002, 2004 by Paul Carter. Traduction par Sébastien Le Ray .
Ce document peut être reproduit et distribué dans sa totalité (en incluant cette note de l'auteur, le
copyright et l'indication de l'autorisation), sous réserve qu'aucuns frais ne soient demandés pour le
document lui-même, sans le consentement de l'auteur. Cela inclut une utilisation raisonnable d'extraits
pour des revues ou de la publicité et les travaux dérivés comme les traductions.
Notez que ces restrictions ne sont pas destinées à empêcher la participation aux frais pour l'impression ou
la copie du document.
Les enseignants sont encouragés à utiliser ce document comme support de cours ; cependant, l'auteur
apprécierait d'être averti dans ce cas.


I. Préface

I-A. Objectif

L'objectif de ce livre est de permettre au lecteur de mieux comprendre comment les ordinateurs fonctionnent réellement à un niveau plus bas que les langages de programmation comme Pascal. En ayant une compréhension plus profonde de la façon dont fonctionnent les ordinateurs, le lecteur peu devenir plus productif dans le développement de logiciels dans des langages de plus haut niveau comme le C et le C++.

Apprendre à programmer en assembleur est un excellent moyen d'atteindre ce but. Les autres livres d'assembleur pour PC apprennent toujours à programmer le processeur 8086 qu'utilisait le PC originel de 1981 ! Le processeur 8086 ne supportait que le mode réel.

Dans ce mode, tout programme peu adresser n'importe quel endroit de la mémoire ou n'importe quel périphérique de l'ordinateur. Ce mode n'est pas utilisable pour un système d'exploitation sécurisé et multitâche. Ce livre parle plutôt de la façon de programmer les processeurs 80386 et plus récents en mode protégé (le mode dans lequel fonctionnent Windows et Linux). Ce mode supporte les fonctionnalités que les systèmes d'exploitation modernes offrent, comme la mémoire virtuelle et la protection mémoire. Il y a plusieurs raisons d'utiliser le mode protégé :

1. Il est plus facile de programmer en mode protégé qu'en mode réel 8086 que les autres livres utilisent ;

2. Tous les systèmes d'exploitation PC modernes fonctionnent en mode protégé ;

3. Il y a des logiciels libres disponibles qui fonctionnent dans ce mode.

Le manque de livres sur la programmation en assembleur PC en mode protégé est la principale raison qui a conduit l'auteur à écrire ce livre.

Comme nous y avons fait allusion ci-dessus, ce texte utilise des logiciels Libres/Open Source : l'assembleur NASM et le compilateur C/C++ DJGPP. Les deux sont disponibles en téléchargement sur Internet. Ce texte parle également de l'utilisation de code assembleur NASM sous Linux et avec les compilateurs C/C++ de Borland et Microsoft sous Windows. Les exemples pour toutes ces plateformes sont disponibles sur mon site Web : http://www.drpaulcarter.com/pcasm.

Vous devez télécharger le code exemple si vous voulez assembler et exécuter la plupart des exemples de ce tutoriel.

Soyez conscient que ce texte ne tente pas de couvrir tous les aspects de la programmation assembleur. L'auteur a essayé de couvrir les sujets les plus importants avec lesquels tous les programmeurs devraient être familiers.

I-B. Remerciements

L'auteur voudrait remercier les nombreux programmeurs qui ont contribué au mouvement Libre/Open Source. Tous les programmes et même ce livre lui-même ont été créés avec des logiciels gratuits. L'auteur voudrait remercier en particulier John S. Fine, Simon Tatham, Julian Hall et les autres développeurs de l'assembleur NASM sur lequel tous les exemples de ce livre sont basés ; DJ Delorie pour le développement du compilateur C/C++ DJGPP utilisé ; les nombreuses personnes qui ont contribué au compilateur GNU gcc sur lequel DJGPP est basé ; Donald Knuth et les autres pour avoir développé les langages composition TEX et LATEX2" qui ont été utilisés pour produire le livre ; Richard Stallman (fondateur de la Free Software Foundation), Linus Torvalds (créateur du noyau Linux) et les autres qui ont créé les logiciels sous-jacents utilisés pour ce travail.

Merci aux personnes suivantes pour leurs corrections :

John S. Fine ; Marcelo Henrique Pinto de Almeida ; Sam Hopkins ; Nick D'Imperio ; Jeremiah Lawrence ; Ed Beroset ; Jerry Gembarowski ; Ziqiang Peng ; Eno Compton ; Josh I Cates ; Mik Miin ; Luke Wallis ; Gaku Ueda ; Brian Heward ; Chad Gorshing ; F. Gotti ; Bob Wilkinson ; Markus Koegel ;Louis Taber.

I-C. Ressources sur Internet

Page de l'auteur : http://www.drpaulcarter.com/.

Page NASM sur SourceForge : http://sourceforge.net/projects/nasm/.

DJGPP : http://www.delorie.com/djgpp.

Assembleur Linux : http://www.linuxassembly.org/.

The Art of Assembly : http://webster.cs.ucr.edu/.

USENET comp.lang.asm.x86

Documentation Intel : http://developer.intel.com/design/Pentium4/documentation.htm.

I-D. Réactions

L'auteur accepte toute réaction sur ce travail.

E-mail : pacman128@gmail.com.

WWW : http://www.drpaulcarter.com/pcasm

Langage Assembleur PC
Les deux auteurs ... Paul A. Carter Sébastien Le Ray
http://www.drpaulcarter.com/pcasm
x Publié le 20 mars 2005 - Mis à jour le 30 janvier 2013
http://rd.himediads.com/fr/ad/developpez.com/
Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de
présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ©2013 Paul A. Carter. Aucune reproduction,
même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse
de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable bénévole de la rubrique Assembleur : Alcatîz- Contacter par email
Developpez.com Copyright © 2000-2014 - www.developpez.com

Copyright © 2001, 2002, 2004 by Paul Carter. Traduction par Sébastien Le Ray .
Ce document peut être reproduit et distribué dans sa totalité (en incluant cette note de l'auteur, le
copyright et l'indication de l'autorisation), sous réserve qu'aucuns frais ne soient demandés pour le
document lui-même, sans le consentement de l'auteur. Cela inclut une utilisation raisonnable d'extraits
pour des revues ou de la publicité et les travaux dérivés comme les traductions.
Notez que ces restrictions ne sont pas destinées à empêcher la participation aux frais pour l'impression ou
la copie du document.
Les enseignants sont encouragés à utiliser ce document comme support de cours ; cependant, l'auteur
apprécierait d'être averti dans ce cas.


II. Introduction

II-A. Systèmes numériques

La mémoire d'un ordinateur est constituée de nombres. Cette mémoire ne stocke pas ces nombres en décimal (base 10). Comme cela simplifie grandement le matériel, les ordinateurs stockent toutes les informations au format binaire (base 2). Tout d'abord, revoyons ensemble le système décimal.

II-A-1. Décimal

Les nombres en base 10 sont constitués de 10 chiffres possibles (0-9).

Chaque chiffre d'un nombre est associé à une puissance de 10 selon sa position dans le nombre. Par exemple :

234 = 2 x 102 + 3 × 101 + 4 × 100

II-A-2. Binaire

Les nombres en base 2 sont composés de deux chiffres possibles (0 et 1). Chaque chiffre est associé à une puissance de 2 selon sa position dans le nombre (un chiffre binaire isolé est appelé bit). Par exemple :

110012 = 1 × 24+ 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20

= 16 + 8 + 1

= 25

Cet exemple montre comment passer du binaire au décimal. Le tableau 1.1 montre la représentation binaire des premiers nombres.

La figure 1.1 montre comment des chiffres binaires individuels (i.e., des bits) sont additionnés. Voici un exemple :

Image non disponible

Si l'on considère la division décimale suivante :

1234 ÷ 10 = 123 r 4

on peut voir que cette division sépare le chiffre le plus à droite du nombre et décale les autres chiffres d'une position vers la droite. Diviser par deux effectue une opération similaire, mais pour les chiffres binaires du nombre.

Considérons la division binaire suivante (1) :

11012 ÷ 102 = 1102 r 1

Cette propriété peut être utilisée pour convertir un nombre décimal en son équivalent binaire comme le montre la Figure 1.2. Cette méthode trouve le chiffre binaire le plus à droite en premier, ce chiffre est appelé le bit le moins signicatif (lsb, least signicant bit). Le chiffre le plus à gauche est appelé le bit le plus signicatif (msb, most signicant bit). L'unité de base de la mémoire consiste en un jeu de 8 bits appelé octet (byte).

Image non disponible

II-A-3. Hexadécimal

Les nombres hexadécimaux utilisent la base 16. L'hexadécimal (ou hexa en abrégé) peut être utilisé comme notation pour les nombres binaires. L'hexa a 16 chiffres possibles. Cela pose un problème, car il n'y a pas de symbole à utiliser pour les chiffres supplémentaires après 9. Par convention, on utilise des lettres pour les représenter. Les 16 chiffres de l'hexa sont 0-9 puis A, B, C, D, E et F. Le chiffre A équivaut à 10 en décimal, B à 11, etc. Chaque chiffre d'un nombre en hexa est associé à une puissance de 16. Par exemple :

2BD16 = 2 × 162 + 11 × 161 + 13 × 160

= 512 + 176 + 13

= 701

Pour convertir un nombre hexa en binaire, convertissez simplement chaque chiffre hexa en un nombre binaire de 4 bits. Par exemple, 24D16 est converti en 0010 0100 11012. Notez que les 0 de tête des 4 bits sont importants ! Si le 0 de tête pour le chiffre du milieu de 24D16 n'est pas utilisé, le résultat est faux. La conversion du binaire vers l'hexa est aussi simple. On effectue la même chose, dans l'autre sens. Convertissez chaque segment de 4 bits du binaire vers l'hexa. Commencez à droite, pas à gauche, du nombre binaire.

Cela permet de s'assurer que le procédé utilise les segments de 4 bits corrects (2)

Exemple :

Image non disponible

Un nombre de 4 bits est appelé quadruplet(nibble). Donc, chaque chiffre hexa correspond à un quadruplet. Deux quadruplets forment un octet et donc un octet peut être représenté par un nombre hexa à deux chiffres. La valeur d'un bit va de 0 à 11111111 en binaire, 0 à FF en hexa et 0 à 255 en décimal.

II-B. Organisation de l'ordinateur

II-B-1. Mémoire

La mémoire est mesurée en kilo-octets ( 210= 1024 octets), mega octets (220= 1 048 576 octets) et gigaoctets (230 = 1 073 741 824 octets).

L'unité mémoire de base est l'octet. Un ordinateur avec 32 mégaoctets de mémoire peut stocker jusqu'à environ 32 millions d'octets d'informations.

Chaque octet en mémoire est étiqueté par un nombre unique appelé son adresse comme le montre la Figure 1.4.

Image non disponible

Souvent, la mémoire est utilisée par bouts plus grands que des octets isolés. Sur l'architecture PC, on a donné des noms à ces portions de mémoire plus grandes, comme le montre le Tableau 1.2.

Image non disponible

Toutes les données en mémoire sont numériques. Les caractères sont stockés en utilisant un code caractère qui fait correspondre des nombres aux caractères. Un des codes caractère les plus connus est appelé ASCII (American Standard Code for Information Interchange, Code Américain Standard pour l'Échange d'Informations). Un nouveau code, plus complet, qui supplante l'ASCII est l'Unicode. Une des différences-clés entre les deux codes est que l'ASCII utilise un octet pour encoder un caractère alors que l'Unicode en utilise deux (ou un mot). Par exemple, l'ASCII fait correspondre l'octet 4116 (6510) au caractère majuscule A ; l'Unicode y fait correspondre le mot 004116. Comme l'ASCII n'utilise qu'un octet, il est limité à 256 caractères différents au maximum (3). L'Unicode étend les valeurs ASCII à des mots et permet de représenter beaucoup plus de caractères. C'est important afin de représenter les caractères de tous les langages du monde.

II-B-2. Le CPU (processeur)

Le processeur (CPU, Central Processing Unit) est le dispositif physique qui exécute les instructions. Les instructions que les processeurs peuvent exécuter sont généralement très simples. Elles peuvent nécessiter que les données sur lesquelles elles agissent soient présentes dans des emplacements de stockage spéciques dans le processeur lui-même appelés registres. Le processeur peut accéder aux données dans les registres plus rapidement qu'aux données en mémoire. Cependant, le nombre de registres d'un processeur est limité, donc le programmeur doit faire attention à n'y conserver que les données actuellement utilisées.

Les instructions que peut exécuter un type de processeur constituent le langage machine. Les programmes machine ont une structure beaucoup plus basique que les langages de plus haut niveau. Les instructions du langage machine sont encodées en nombres bruts, pas en format texte lisible. Un processeur doit être capable de décoder les instructions très rapidement pour fonctionner efficacement. Le langage machine est conçu avec ce but en tête, pas pour être facilement déchiffrable par les humains. Les programmes écrits dans d'autres langages doivent être convertis dans le langage machine programme qui traduit les programmes écrits dans un langage de programmation en langage machine d'une architecture d'ordinateur particulière. En général, chaque type de processeur a son propre langage machine unique. C'est une des raisons pour lesquelles un programme écrit pour Mac ne peut pas être exécuté sur un PC.

Les ordinateurs utilisent une horloge pour synchroniser l'exécution des instructions. L'horloge tourne à une fréquence fixée (appelée vitesse d'horloge). Lorsque vous achetez un ordinateur à 1,5GHz, 1,5GHz est la fréquence de cette horloge.

GHz signifie gigahertz ou un milliard de cycles par seconde. Un processeur à 1,5GHz a 1,5 milliard d'impulsions horloge par seconde.

L'horloge ne décompte pas les minutes et les secondes. Elle bat simplement à un rythme constant. Les composants électroniques du processeur utilisent les pulsations pour effectuer leurs opérations correctement, comme le battement d'un métronome aide à jouer de la musique à un rythme correct. Le nombre de battements (ou, comme on les appelle couramment cycles) que requiert une instruction dépend du modèle et de la génération du processeur. Le nombre de cycles dépend de l'instruction.

II-B-3. La famille des processeurs 80x86

Les PC contiennent un processeur de la famille des Intel 80x86 (ou un clone). Les processeurs de cette famille ont tous des fonctionnalités en commun, y compris un langage machine de base. Cependant, les membres les plus récents améliorent grandement les fonctionnalités.

8088,8086 : ces processeurs, du point de vue de la programmation sont identiques. Ils étaient les processeurs utilisés dans les tout premiers PC. Ils offrent plusieurs registres 16 bits : AX, BX, CX, DX, SI, DI, BP, SP, CS, DS, SS, ES, IP, FLAGS. Ils ne supportent que jusqu'à 1Mo de mémoire et n'opèrent qu'en mode réel. Dans ce mode, un programme peut accéder à n'importe quelle adresse mémoire, même la mémoire des autres programmes ! Cela rend le débogage et la sécurité très difficiles ! De plus, la mémoire du programme doit être divisée en segments. Chaque segment ne peut pas dépasser les 64 Ko.

80286 : ce processeur était utilisé dans les PC de type AT. Il apporte quelques nouvelles instructions au langage machine de base des 8088/86. Cependant, sa principale nouvelle fonctionnalité est le mode protégé 16 bits. Dans ce mode, il peut accéder jusqu'à 16 Mo de mémoire et empêcher les programmes d'accéder à la mémoire des uns et des autres. Cependant, les programmes sont toujours divisés en segments qui ne peuvent pas dépasser les 64 Ko.

80386 : ce processeur a grandement amélioré le 80286. Tout d'abord, il étend la plupart des registres à 32 bits (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP) et ajoute deux nouveaux registres 16 bits : FS et GS.

Image non disponible

Il ajoute également un nouveau mode protégé 32 bits. Dans ce mode, il peut accéder jusqu'à 4 Go de mémoire. Les programmes sont encore divisés en segments, mais maintenant chaque segment peut également faire jusqu'à 4 Go !

80486/Pentium/Pentium Pro : ces membres de la famille 80x86 apportent très peu de nouvelles fonctionnalités. Ils accélèrent principalement l'exécution des instructions.

Pentium MMX : ce processeur ajoute les instructions MMX (Multi Media eXentions) au Pentium. Ces instructions peuvent accélérer des opérations graphiques courantes.

Pentium II : c'est un processeur Pentium Pro avec les instructions MMX (le Pentium III est grossièrement un Pentium II plus rapide).

II-B-4. Regitres 16 bits du 8086

Le processeur 8086 original fournissait quatre registres généraux de 16 bits : AX, BX, CX et DX. Chacun de ces registres peut être décomposé en deux registres de 8 bits. Par exemple, le registre AX pouvait être décomposé en AH et AL comme le montre la Figure 1.5. Le registre AH contient les 8 bits de poids fort de AX et AL contient les 8 bits de poids faible. Souvent, AH et AL sont utilisés comme des registres d'un octet indépendants ; cependant, il est important de réaliser qu'ils ne sont pas indépendants de AX. Changer la valeur de AX changera les valeurs de AL et BL et vice versa. Les registres généraux sont utilisés dans beaucoup de déplacements de données et instructions arithmétiques.

Il y a deux registres d'index de 16 bits : SI et DI. Ils sont souvent utilisés comme des pointeurs, mais peuvent être utilisés pour la plupart des mêmes choses que les registres généraux. Cependant, ils ne peuvent pas être décomposés en registres de 8 bits.

Les registres 16 bits BP et SP sont utilisés pour pointer sur des données dans la pile du langage machine et sont appelés le pointeur de base et le pointeur de pile, respectivement. Nous en reparlerons plus tard.

Les registres 16 bits CS, DS, SS et ES sont des registres de segment. Ils indiquent quelle zone de la mémoire est utilisée pour les différentes parties d'un programme. CS signifie Code Segment, DS Data Segment, SS Stack Segment (segment de pile) et ES Extra Segment. ES est utilisé en tant que registre de segment temporaire. Des détails sur ces registres se trouvent dans les sections II.B.6 et II.B.7.

Le registre de pointeur d'instruction (IP) est utilisé avec le registre CS pour mémoriser l'adresse de la prochaine instruction à exécuter par le processeur.

Normalement, lorsqu'une instruction est exécutée, IP est incrémenté pour pointer vers la prochaine instruction en mémoire.

Le registre FLAGS stocke des informations importantes sur les résultats d'une instruction précédente. Ces résultats sont stockés comme des bits individuels dans le registre. Par exemple, le bit Z est positionné à 1 si le résultat de l'instruction précédente était 0 ou à 0 sinon. Toutes les instructions ne modifient pas les bits dans FLAGS, consultez le tableau dans l'appendice pour voir comment chaque instruction affecte le registre FLAGS.

II-B-5. Registres 32 bits du 80386

Les processeurs 80386 et plus récents ont des registres étendus. Par exemple le registre AX 16 bits est étendu à 32 bits. Pour la compatibilité ascendante, AX fait toujours référence au registre 16 bits et on utilise EAX pour faire référence au registre 32 bits. AX représente les 16 bits de poids faible de EAX tout comme AL représente les 8 bits de poids faible de AX (et de EAX). Il n'y a pas moyen d'accéder aux 16 bits de poids fort de EAX directement. Les autres registres étendus sont EBX, ECX, EDX, ESI et EDI.

La plupart des autres registres sont également étendus. BP devient EBP ; SP devient ESP ; FLAGS devient EFLAGS et IP devient EIP. Cependant, contrairement aux registres généraux et d'index, en mode protégé 32 bits (dont nous parlons plus loin) seules les versions étendues de ces registres sont utilisées.

Les registres de segment sont toujours sur 16 bits dans le 80386. Il y a également deux registres de segment supplémentaires : FS et GS. Leurs noms n'ont pas de signification particulière. Ce sont des segments temporaires supplémentaires (comme ES).

Une des définitions du terme mot se réfère à la taille des registres de données du processeur. Pour la famille du 80x86, le terme est désormais un peu confus. Dans le Tableau 1.2, on voit que le terme mot est déni comme faisant 2 octets (ou 16 bits). Cette signification lui a été attribuée lorsque le premier 8086 est apparu. Lorsque le 80386 a été développé, il a été décidé de laisser la définition de mot inchangée, même si la taille des registres avait changé.

II-B-6. Mode réel

En mode réel, la mémoire est limitée à seulement un mégaoctet (220 octets).

Alors d'où vient l'infâme limite des 640 Ko du DOS ? Le BIOS requiert une partie des 1 Mo pour son propre code et pour les périphériques matériels comme l'écran.

Les adresses valides vont de 00000 à FFFFF (en hexa). Ces adresses nécessitent un nombre sur 20 bits. Cependant, un nombre de 20 bits ne tiendrait dans aucun des registres 16 bits du 8086. Intel a résolu le problème, en utilisant deux valeurs de 16 bits pour déterminer une adresse. La première valeur de 16 bits est appelée le sélecteur. Les valeurs du sélecteur doivent être stockées dans des registres de segment. La seconde valeur de 16 bits est appelée le déplacement (offset). L'adresse physique identifiée par un couple sélecteur/déplacement 32 bits est calculée par la formule

16 * sélecteur + déplacement

Multiplier par 16 en hexa est facile, il suffit d'ajouter un 0 à la droite du nombre. Par exemple, l'adresse physique référencée par 047C:0048 est obtenue de la façon suivante :

047C0

+0048

04808

De fait, la valeur du sélecteur est un numéro de paragraphe (voir Tableau 1.2).

Les adresses réelles segmentées ont des inconvénients :

  • une seule valeur de sélecteur peut seulement référencer 64Ko de mémoire (la limite supérieure d'un déplacement de 16 bits). Que se passe-t-il si un programme a plus de 64 Ko de code ? Une seule valeur de CS ne peut pas être utilisée pour toute l'exécution du programme. Le programme doit être divisé en sections (appelées segments) de moins de 64 Ko. Lorsque l'exécution passe d'un segment à l'autre, la valeur de CS doit être changée. Des problèmes similaires surviennent avec de grandes quantités de données et le registre DS. Cela peut être très gênant ! ;
  • chaque octet en mémoire n'a pas une adresse segmentée unique. L'adresse physique 04808 peut être référencée par 047C:0048, 047D:0038, 047E:0028 ou 047B:0058. Cela complique la comparaison d'adresses segmentées.

II-B-7. Mode protégé 16 bits

Dans le mode protégé 16 bits du 80286, les valeurs du sélecteur sont interprétées de façon totalement différente par rapport au mode réel. En mode réel, la valeur d'un sélecteur est un numéro de paragraphe en mémoire.

En mode protégé, un sélecteur est un indice dans un tableau de descripteurs. Dans les deux modes, les programmes sont divisés en segments. En mode réel, ces segments sont à des positions fixes en mémoire et le sélecteur indique le numéro de paragraphe auquel commence le segment. En mode protégé, les segments ne sont pas à des positions fixes en mémoire physique. De fait, ils n'ont même pas besoin d'être en mémoire du tout !

Le mode protégé utilise une technique appelée mémoire virtuelle. L'idée de base d'un système de mémoire virtuelle est de ne garder en mémoire que les programmes et les données actuellement utilisés. Le reste des données et du code sont stockés temporairement sur le disque jusqu'à ce qu'on ait à nouveau besoin d'eux. Dans le mode protégé 16 bits, les segments sont déplacés entre la mémoire et le disque selon les besoins. Lorsqu'un segment est rechargé en mémoire depuis le disque, il est très probable qu'il sera à un endroit en mémoire différent de celui où il était avant d'être placé sur le disque.

Tout ceci est effectué de façon transparente par le système d'exploitation. Le programme n'a pas à être écrit différemment pour que la mémoire virtuelle fonctionne.

En mode protégé, chaque segment est assigné à une entrée dans un tableau de descripteurs. Cette entrée contient toutes les informations dont le système a besoin à propos du segment. Ces informations indiquent : s'il est actuellement en mémoire ; s'il est en mémoire, où il se trouve ; les droits d'accès (p.e., lecture seule). L'indice de l'entrée du segment est la valeur du sélecteur stockée dans les registres de segment.

Un journaliste PC bien connu a baptisé le processeur 286 « cerveau mort » (brain dead).

Un gros inconvénient du mode protégé 16 bits est que les déplacements sont toujours des quantités sur 16 bits. En conséquence, les tailles de segment sont toujours limitées au plus à 64 Ko. Cela rend l'utilisation de grands tableaux problématique.

II-B-8. Mode protégé 32 bits

Le 80386 a introduit le mode protégé 32 bits. Il y a deux différences majeures entre les modes protégés 32 bits du 386 et 16 bits du 286 :

1. Les déplacements sont étendus à 32 bits. Cela permet à un déplacement d'aller jusqu'à 4 milliards. Ainsi, les segments peuvent avoir des tailles jusqu'à 4 Go ;

2. Les segments peuvent être divisés en unités plus petites de 4 Ko appelées pages. Le système de mémoire virtuelle fonctionne maintenant avec des pages plutôt qu'avec des segments. Cela implique que seules certaines parties d'un segment peuvent être présentes en mémoire à un instant donné. En mode 16 bits du 286, soit le segment en entier est en mémoire, soit rien n'y est. Ce qui n'aurait pas été pratique avec les segments plus grands que permet le mode 32 bits.

Dans Windows 3.x, le mode standard fait référence au mode protégé 16 bits du 286 et le mode amélioré (enhanced) fait référence au mode 32 bits.

Windows 9X, Windows NT/2000/XP, OS/2 et Linux fonctionnent tous en mode protégé 32 bits paginé.

II-B-9. Interruptions

Quelques fois, le flot ordinaire d'un programme doit être interrompu pour traiter des évènements qui requièrent une réponse rapide. Le matériel d'un ordinateur offre un mécanisme appelé interruptions pour gérer ces évènements.

Par exemple, lorsqu'une souris est déplacée, la souris interrompt le programme en cours pour gérer le déplacement de la souris (pour déplacer le curseur, etc.). Les interruptions provoquent le passage du contrôle à un gestionnaire d'interruptions. Les gestionnaires d'interruptions sont des routines qui traitent une interruption. Chaque type d'interruption est assignée à un nombre entier. Au début de la mémoire physique, réside un tableau de vecteurs d'interruptions qui contient les adresses segmentées des gestionnaires d'interruptions. Le numéro d'une interruption est essentiellement un indice dans ce tableau.

Les interruptions externes proviennent de l'extérieur du processeur (la souris est un exemple de ce type). Beaucoup de périphériques d'E/S soulèvent des interruptions (p.e., le clavier, le timer, les lecteurs de disque, le CD-ROM et les cartes son). Les interruptions internes sont soulevées depuis le processeur, à cause d'une erreur ou d'une instruction d'interruption.

Les interruptions erreur sont également appelées traps. Les interruptions générées par l'instruction d'interruption sont également appelées interruptions logicielles. Le DOS utilise ce type d'interruption pour implémenter son API (Application Programming Interface). Les systèmes d'exploitation plus récents (comme Windows et Unix) utilisent une interface basée sur C (4).

Beaucoup de gestionnaires d'interruptions redonnent le contrôle au programme interrompu lorsqu'ils se terminent. Ils restaurent tous les registres aux valeurs qu'ils avaient avant l'interruption. Ainsi, le programme interrompu s'exécute comme si rien n'était arrivé (excepté qu'il perd quelques cycles processeur). Les traps ne reviennent généralement jamais. Souvent, elles arrêtent le programme.

II-C. Langage assembleur

II-C-1. Langage machine

Chaque type de processeur comprend son propre langage machine. Les instructions dans le langage machine sont des nombres stockés sous la forme d'octets en mémoire. Chaque instruction a son propre code numérique unique appelé code d'opération ou opcode (operation code) en raccourci. Les instructions des processeurs 80x86 varient en taille. L'opcode est toujours au début de l'instruction. Beaucoup d'instructions comprennent également les données (p.e. des constantes ou des adresses) utilisées par l'instruction.

Le langage machine est très difficile à programmer directement. Déchiffrer la signification d'instructions codées numériquement est fatigant pour des humains. Par exemple, l'instruction qui dit d'ajouter les registres EAX et EBX et de stocker le résultat dans EAX est encodée par les codes hexadécimaux suivants :

03 C3

C'est très peu clair. Heureusement, un programme appelé un assembleur peut faire ce travail laborieux à la place du programmeur.

II-C-2. Langage d'assembleur

Un programme en langage d'assembleur est stocké sous la forme de texte (comme un programme dans un langage de plus haut niveau). Chaque instruction assembleur représente exactement une instruction machine. Par exemple, l'instruction d'addition décrite ci-dessus serait représentée en langage assembleur comme suit :

add eax, ebx

Ici, la signification de l'instruction est beaucoup plus claire qu'en code machine.

Le mot add est un mnémonique pour l'instruction d'addition. La forme générale d'une instruction assembleur est :

mnémonique opérande(s)

Un assembleur est un programme qui lit un fichier texte avec des instructions assembleur et convertit l'assembleur en code machine. Les compilateurs sont des programmes qui font des conversions similaires pour les langages de programmation de haut niveau. Un assembleur est beaucoup plus simple qu'un compilateur.

Cela a pris plusieurs années aux scientifiques de l'informatique pour concevoir le simple fait d'écrire un compilateur !

Chaque instruction du langage d'assembleur représente directement une instruction machine. Les instructions d'un langage de plus haut niveau sont beaucoup plus complexes et peuvent requérir beaucoup d'instructions machine.

Une autre différence importante entre l'assembleur et les langages de haut niveau est que comme chaque type de processeur a son propre langage machine, il a également son propre langage d'assemblage. Porter des programmes assembleur entre différentes architectures d'ordinateur est beaucoup plus difficile qu'avec un langage de haut niveau.

Les exemples de ce livre utilisent le Netwide Assembler ou NASM en

raccourci. Il est disponible gratuitement sur Internet (voyez la préface pour l'URL). Des assembleurs plus courants sont l'Assembleur de Microsoft (MASM) ou l'Assembleur de Borland (TASM). Il y a quelques différences de syntaxe entre MASM/TASM et NASM.

II-C-3. Opérandes d'instruction

Les instructions en code machine ont un nombre et un type variables d'opérandes ; cependant, en général, chaque instruction a un nombre fixé d'opérandes (0 à 3). Les opérandes peuvent avoir les types suivants :

registre : ces opérandes font directement référence au contenu des registres du processeur ;

mémoire : ils font référence aux données en mémoire. L'adresse de la donnée peut être une constante codée en dur dans l'instruction ou calculée en utilisant les valeurs des registres. Les adresses sont toujours des déplacements relatifs au début d'un segment ;

immédiat : ce sont des valeurs fixes qui sont listées dans l'instruction elle-même. Elles sont stockées dans l'instruction (dans le segment de code), pas dans le segment de données ;

implicite : ces opérandes ne sont pas entrés explicitement. Par exemple, l'instruction d'incrémentation ajoute 1 à un registre ou à la mémoire. Le 1 est implicite.

II-C-4. Instructions de base

L'instruction la plus basique est l'instruction MOV. Elle déplace les données d'un endroit à un autre (comme l'opérateur d'assignement dans un langage de haut niveau). Elle prend deux opérandes :

Sélectionnez

mov dest, src

La donnée spécifiée par src est copiée vers dest. Une restriction est que les deux opérandes ne peuvent pas être tous deux des opérandes mémoire.

Cela nous montre un autre caprice de l'assembleur. Il y a souvent des règles quelque peu arbitraires sur la façon dont les différentes instructions sont utilisées. Les opérandes doivent également avoir la même taille. La valeur de AX ne peut pas être stockée dans BL.

Voici un exemple (les points-virgules marquent un commentaire) :

Sélectionnez

mov eax, 3 ; stocke 3 dans le registre EAX (3 est un operande immediat)

mov bx, ax ; stocke la valeur de AX dans le registre BX

L'instruction ADD est utilisée pour additionner des entiers.

Sélectionnez

add eax, 4 ; eax = eax + 4

add al, ah ; al = al + ah

L'instruction SUB soustrait des entiers.

Sélectionnez

sub bx, 10 ; bx = bx - 10

sub ebx, edi ; ebx = ebx - edi

Les instructions INC et DEC incrémentent ou décrémentent les valeurs de 1. Comme le 1 est un opérande implicite, le code machine pour INC et DEC est plus petit que celui des instructions ADD et SUB équivalentes.

Sélectionnez

inc ecx ; ecx++

dec dl ; dl--

II-C-5. Directives

Une directive est destinée à l'assembleur, pas au processeur. Les directives sont généralement utilisées pour indiquer à l'assembleur de faire quelque chose ou pour l'informer de quelque chose. Elles ne sont pas traduites en code machine. Les utilisations courantes des directives sont :
  • la définition de constantes ;
  • la définition de mémoire pour stocker des données ;
  • grouper la mémoire en segment ;
  • inclure des codes source de façon conditionnelle ;
  • inclure d'autres fichiers.

Le code NASM est analysé par un préprocesseur, exactement comme en C. Il y a beaucoup de commandes identiques à celles du préprocesseur C.

Cependant, les directives du préprocesseur NASM commencent par un % au lieu d'un # comme en C.

II-C-5-a. La directive equ

La directive equ peut être utilisée pour définir un symbole. Les symboles sont des constantes nommées qui peuvent être utilisées dans le programme assembleur. Le format est le suivant :

Sélectionnez

symbole equ valeur

Les valeurs des symboles ne peuvent pas être redéfinies plus tard.

II-C-5-b. La directive ?ne

Cette directive est semblable à la directive #define du C. Elle est le plus souvent utilisée pour définir des macros, exactement comme en C.

Sélectionnez

?fine SIZE 100
mov eax, SIZE

Image non disponible

Le code ci-dessus définit une macro appelée SIZE et montre son utilisation dans une instruction MOV. Les macros sont plus flexibles que les symboles de deux façons. Elles peuvent être redéfinies et peuvent être plus que de simples nombres constants.

II-C-5-c. Directives de données

Les directives de données sont utilisées dans les segments de données pour réserver de la place en mémoire. Il y a deux façons de réserver de la mémoire. La première ne fait qu'allouer la place pour les données ; a seconde alloue la place et donne une valeur initiale. La première méthode utilise une des directives RESX. Le X est remplacé par une lettre qui détermine la taille de l'objet (ou des objets) qui sera stocké. Le Tableau 1.3 montre les valeurs possibles.

La seconde méthode (qui définit une valeur initiale) utilise une des directives DX. Les lettres X sont les mêmes que celles de la directive RESX.

Il est très courant de marquer les emplacements mémoire avec des labels (étiquettes). Les labels permettent de faire référence facilement aux emplacements mémoire dans le code. Voici quelques exemples :

Sélectionnez

L1 db 0 ; octet libelle L1 avec une valeur initiale de 0
L2 dw 1000 ; mot labelle L2 avec une valeur initiale de 1000
L3 db 110101b ; octet initialise a la valeur binaire 110101 (53 en decimal)
L4 db 12h ; octet initialise a la valeur hexa 12 (18 en decimal)
L5 db 17o ; octet initialise a la valeur octale 17 (15 en decimal)
L6 dd 1A92h ; double mot initialise a la valeur hexa 1A92
L7 resb 1 ; 1 octet non initialise
L8 db "A" ; octet initialise avec le code ASCII du A (65)

Les doubles et simples quotes sont traitées de la même façon. Les définitions de données consécutives sont stockées séquentiellement en mémoire. C'est-à-dire que le mot L2 est stocké immédiatement après L1 en mémoire. Des séquences de mémoire peuvent également être définies.

Sélectionnez

L9 db 0, 1, 2, 3 ; definit 4 octets
L10 db "w", "o", "r", 'd', 0 ; definit une chaine C = "word"
L11 db 'word', 0 ; idem L10

La directive DD peut être utilisée pour définir à la fois des entiers et des constantes à virgule flottante en simple précision (5). Cependant, la directive DQ ne peut être utilisée que pour définir des constantes à virgule flottante en double précision.

Pour les grandes séquences, la directive de NASM TIMES est souvent utile. Cette directive répète son opérande un certain nombre de fois. Par exemple :

Sélectionnez

L12 times 100 db 0 ; equivalent a 100 (db 0)
L13 resw 100 ; reserve de la place pour 100 mots

Souvenez-vous que les labels peuvent être utilisés pour faire référence à des données dans le code. Il y a deux façons d'utiliser les labels. Si un label simple est utilisé, il fait référence à l'adresse (ou offset) de la donnée.

Si le label est placé entre crochets ([ ]), il est interprété comme la donnée à cette adresse. En d'autres termes, il faut considérer un label comme un pointeur vers la donnée et les crochets déréférencent le pointeur, exactement comme l'astérisque en C (MASM/TASM utilisent une convention différente).

En mode 32 bits, les adresses sont sur 32 bits. Voici quelques exemples :

Sélectionnez

1.           mov al, [L1] ; Copie l'octet situe en L1 dans AL
2.           mov eax, L1 ; EAX = addresse de l'octet en L1
3.           mov [L1], ah ; copie AH dans l'octet en L1
4.            mov eax, [L6] ; copie le double mot en L6 dans EAX
5.            add eax, [L6] ; EAX = EAX + double mot en L6
6.           add [L6], eax ; double mot en L6 += EAX
7.           mov al, [L6] ; copie le premier octet du double mot en L6 dans AL

La ligne 7 de cet exemple montre une propriété importante de NASM. L'assembleur ne garde pas de trace du type de données auquel se réfère le label. C'est au programmeur de s'assurer qu'il (ou elle) utilise un label correctement.

Plus tard, il sera courant de stocker des adresses dans les registres et utiliser les registres comme un pointeur en C. Là encore, aucune vérification n'est faite pour savoir si le pointeur est utilisé correctement. À ce niveau, l'assembleur est beaucoup plus sujet à erreur que le C.

Considérons l'instruction suivante :

Sélectionnez

1.           mov [L6], 1 ; stocke 1 en L6

Cette instruction produit une erreur operation size not specified. Pourquoi ?

Parce que l'assembleur ne sait pas s'il doit considérer 1 comme un octet, un mot ou un double mot. Pour réparer cela, il faut ajouter un spécificateur de taille :

Sélectionnez

1.           mov dword [L6], 1 ; stocke 1 en L6

Cela indique à l'assembleur de stocker un 1 dans le double mot qui commence en L6. Les autres spécificateurs de taille sont : BYTE, WORD, QWORD et TWORD (6).

II-C-6. Entrées et sorties

Les entrées et sorties sont des activités très dépendantes du système. Cela implique un interfaçage avec le matériel. Les langages de plus haut niveau, comme C, fournissent des bibliothèques standard de routines pour une interface de programmation simple, uniforme pour les E/S. Les langages d'assembleur n'ont pas de bibliothèque standard. Ils doivent soit accéder directement au matériel (avec une opération privilégiée en mode protégé) ou utiliser les routines de bas niveau éventuellement fournies par le système d'exploitation.

Il est très courant pour les routines assembleur d'être interfacées avec du C. Un des avantages de cela est que le code assembleur peut utiliser les routines d'E/S de la bibliothèque standard du C. Cependant, il faut connaître les règles de passage des informations aux routines que le C utilise.

Ces règles sont trop compliquées pour en parler ici (nous en parlerons plus tard !). Pour simplifier les E/S, l'auteur a développé ses propres routines qui masquent les règles complexes du C et fournissent une interface beaucoup plus simple. Le Tableau 1.4 décrit les routines fournies. Toutes les routines préservent les valeurs de tous les registres, excepté les routines read. Ces routines modifient la valeur du registre EAX. Pour utiliser ces routines, il faut inclure un fichier contenant les informations dont l'assembleur a besoin pour les utiliser. Pour inclure un fichier dans NASM, utilisez la directive du préprocesseur %include. La ligne suivante inclut le fichier requis par les routines d'E/S de l'auteur (7) :

Sélectionnez

1.           %include "asm_io.inc"

Pour utiliser une de ces routines d'affichage, il faut charger EAX avec la valeur correcte et utiliser une instruction CALL pour l'invoquer. L'instruction CALL est équivalente à un appel de fonction dans un langage de haut niveau.

Elle saute à une autre portion de code, mais revient à son origine une fois la routine terminée. Le programme d'exemple ci-dessous montre plusieurs exemples d'appel à ces routines d'E/S.

Image non disponible

II-C-7. Débogage

La bibliothèque de l'auteur contient également quelques routines utiles pour déboguer les programmes. Ces routines de débogage affichent des informations sur l'état de l'ordinateur sans le modifier. Ces routines sont en fait des macros qui sauvegardent l'état courant du processeur puis font appel à une sous-routine. Les macros sont définies dans le fichier asm_io.inc dont nous avons parlé plus haut. Les macros sont utilisées comme des instructions ordinaires. Les opérandes des macros sont séparés par des virgules.

Il y a quatre routines de débogage nommées dump_regs, dump_mem, dump_stack et dump_math ; elles affichent respectivement les valeurs des registres, de la mémoire, de la pile et du coprocesseur arithmétique.

dump_regs : cette macro affiche les valeurs des registres (en hexadécimal) de l'ordinateur sur stdout (i.e. l'écran). Elle affiche également les bits positionnés dans le registre FLAGS (8). Par exemple, si le drapeau zéro (zero ag) est à 1, ZF est affiché. S'il est à 0, il n'est pas affiché. Elle prend en argument un entier qui est affiché également. Cela peut aider à distinguer la sortie de différentes commandes dump_regs.

dump_mem : cette macro affiche les valeurs d'une région de la mémoire (en hexadécimal et également en caractères ASCII). Elle prend trois arguments séparés par des virgules. Le premier est un entier utilisé pour étiqueter la sortie (comme l'argument de dump_regs). Le second argument est l'adresse à afficher (cela peut être un label). Le dernier argument est le nombre de paragraphes de 16 octets à afficher après l'adresse. La mémoire affichée commencera au premier multiple de paragraphe avant l'adresse demandée.

dump_stack : cette macro affiche les valeurs de la pile du processeur (la pile sera présentée dans le Chapitre V). La pile est organisée en doubles-mots et cette routine les affiche de cette façon. Elle prend trois arguments délimités par des virgules. Le premier est un entier (comme pour

dump_regs). Le second est le nombre de doubles-mots à afficher après l'adresse que le registre EBP contient et le troisième argument est le nombre de doubles-mots à afficher avant l'adresse contenue dans EBP.

dump_math : cette macro affiche les valeurs des registres du coprocesseur arithmétique. Elle ne prend qu'un entier en argument qui est utilisé pour étiqueter la sortie comme le fait l'argument de dump_regs.

II-D. Créer un programme

Aujourd'hui, il est très peu courant de créer un programme autonome écrit complètement en langage assembleur. L'assembleur est habituellement utilisé pour optimiser certaines routines critiques. Pourquoi ? Il est beaucoup plus simple de programmer dans un langage de plus haut niveau qu'en assembleur.

De plus, utiliser l'assembleur rend le programme très dur à porter sur d'autres plateformes. En fait, il est rare d'utiliser l'assembleur tout court.

Alors, pourquoi apprendre l'assembleur ?

1. Quelques fois, le code écrit en assembleur peut être plus rapide et plus compact que le code généré par un compilateur.

2. L'assembleur permet l'accès à des fonctionnalités matérielles du système directement qu'il pourrait être difficile ou impossible à utiliser depuis un langage de plus haut niveau.

3. Apprendre à programmer en assembleur aide à acquérir une compréhension plus profonde de la façon dont fonctionne un ordinateur.

4. Apprendre à programmer en assembleur aide à mieux comprendre comment les compilateurs et les langages de haut niveau comme C fonctionnent.

Ces deux derniers points démontrent qu'apprendre l'assembleur peut être utile même si on ne programme jamais dans ce langage plus tard. En fait, l'auteur programme rarement en assembleur, mais il utilise les leçons qu'il en a tirées tous les jours.

II-D-1. Premier programme

Les programmes qui suivront dans ce texte partiront tous du programme de lancement en C de la Figure 1.6.

Image non disponible

Il appelle simplement une autre fonction nommée asm_main. C'est la routine qui sera écrite en assembler proprement dit. Il y a plusieurs avantages à utiliser un programme de lancement en C. Tout d'abord, cela laisse le système du C initialiser le programme de façon à fonctionner correctement en mode protégé. Tous les segments et les registres correspondants seront initialisés par le C. L'assembleur n'aura pas à se préoccuper de cela. Ensuite, la bibliothèque du C pourra être utilisée par le code assembleur. Les routines d'E/S de l'auteur en tirent parti. Elles utilisent les fonctions d'E/S du C (printf, etc.). L'exemple suivant montre un programme assembleur simple.

first.asm

Sélectionnez

asm
1 ; fichier : first.asm
2 ; Premier programme assembleur. Ce programme attend la saisie de deux
3 ; entiers et affiche leur somme.
4 ;
5 ; Pour creer l'executable en utilisant djgpp :
6 ; nasm -f coff first.asm
7 ; gcc -o first first.o driver.c asm_io.o
8
9 %include "asm_io.inc"
10 ;
11 ; Les donnees initialisees sont placees dans le segment .data
12 ;
13 segment .data
14 ;
15 ; Ces labels referencent les chaines utilisees pour l'affichage
16 ;
17 prompt1 db "Entrez un nombre : ", 0 ; N'oubliez pas le 0 final
18 prompt2 db "Entrez un autre nombre : ", 0
19 outmsg1 db "Vous avez entre ", 0
20 outmsg2 db " et ", 0
21 outmsg3 db ", leur somme vaut ", 0
22
23 ;
24 ; Les donnees non initialisees sont placees dans le segment .bss
25 ;
26 segment .bss
27 ;
28 ; Ces labels referencent les doubles-mots utilises pour stocker les entrees
29 ;
30 input1 resd 1
31 input2 resd 1
32
33 ;
34 ; Le code est place dans le segment .text
35 ;
36 segment .text
37 global _asm_main
38 _asm_main :
39 enter 0,0 ; initialisation
40 pusha
41
42 mov eax, prompt1 ; affiche un message
43 call print_string
44
45 call read_int ; lit un entier
46 mov [input1], eax ; le stocke dans input1
47
48 mov eax, prompt2 ; affiche un message
49 call print_string
50
51 call read_int ; lit un entier
52 mov [input2], eax ; le stocke dans input2
53
54 mov eax, [input1] ; eax = dword en input1
55 add eax, [input2] ; eax += dword en input2
56 mov ebx, eax ; ebx = eax
57
58 dump_regs 1 ; affiche les valeurs des registres
59 dump_mem 2, outmsg1, 1 ; affiche le contenu de la memoire
60 ;
61 ; Ce qui suit affiche le message resultat en plusieurs etapes
62 ;
63 mov eax, outmsg1
64 call print_string ; affiche le premier message
65 mov eax, [input1]
66 call print_int ; affiche input1
67 mov eax, outmsg2
68 call print_string ; affiche le second message
69 mov eax, [input2]
70 call print_int ; affiche input2
71 mov eax, outmsg3
72 call print_string ; affiche le troisieme message
73 mov eax, ebx
74 call print_int ; affiche la somme (ebx)
75 call print_nl ; affiche une nouvelle ligne
76
77 popa
78 mov eax, 0 ; retourne dans le programme C
79 leave
80 ret

La ligne 13 du programme définit une section qui spécifie la mémoire à stocker dans le segment de données (dont le nom est .data). Seules les données initialisées doivent être définies dans ce segment.

Dans les lignes 17 à 21, plusieurs chaînes sont déclarées. Elles seront affichées avec la bibliothèque C et doivent donc se terminer par un caractère null (code ASCII 0). Souvenez-vous qu'il y a une grande différence entre 0 et '0'.

Les données non initialisées doivent être déclarées dans le segment bss (appelé .bss à la ligne 26). Ce segment tient son nom d'un vieil opérateur assembleur UNIX qui signifiait block started by symbol. Il y a également un segment de pile. Nous en parlerons plus tard.

Le segment de code est appelé .text historiquement. C'est là que les instructions sont placées. Notez que le label de code pour la routine main (ligne 38) a un préfixe de soulignement. Cela fait partie de la convention d'appel C. Cette convention spécifie les règles que le C utilise lorsqu'il compile le code. Il est très important de connaître cette convention lorsque l'on interface du C et de l'assembleur. Plus loin, la convention sera présentée dans son intégralité ; cependant, pour l'instant, il suffit de savoir que tous les symboles C (i.e., les fonctions et les variables globales) ont un préfixe de soulignement qui leur est ajouté par le compilateur C (cette règle s'applique spécifiquement pour DOS/Windows, le compilateur C Linux n'ajoute rien du tout aux noms des symboles).

La directive global ligne 37 indique à l'assembleur de rendre le label _asm_main global. Contrairement au C, les labels ont une portée interne par défaut. Cela signifie que seul le code du même module peut utiliser le label.

La directive global donne au(x) label(s) spécifié(s) une portée externe. Ce type de label peut être accédé par n'importe quel module du programme.

Le module asm_io déclare les labels print_int, et.al. comme étant globaux. C'est pourquoi l'on peut les utiliser dans le module first.asm.

II-D-2. Dépendance vis-à-vis du compilateur

Le code assembleur ci-dessus est spécifique au compilateur C/C++ GNU (9) gratuit DJGPP(10).

Ce compilateur peut être téléchargé gratuitement depuis Internet. Il nécessite un PC à base de 386 ou plus et tourne sous DOS, Windows 95/98 ou NT. Ce compilateur utilise des fichiers objet au format COFF (Common Object File Format). Pour assembler le fichier au format COFF, utilisez l'option -f coff avec nasm (comme l'indiquent les commentaires du code). L'extension du fichier objet sera o.

Le compilateur C Linux est également un compilateur GNU. Pour convertir le code ci-dessus afin qu'il tourne sous Linux, retirez simplement les préfixes de soulignement aux lignes 37 et 38. Linux utilise le format ELF (Executable and Linkable Format) pour les fichiers objet. Utilisez l'option -f elf pour Linux. Il produit également un fichier objet avec l'extension o.

Les fichiers spécifiques aucompilateur, disponibles sur le site de l'auteur, ont déjà été modifiés pour fonctionner avec le compilateur approprié.

Borland C/C++ est un autre compilateur populaire. Il utilise le format Microsoft OMF pour les fichiers objets. Utilisez l'option -f obj pour les compilateurs Borland. L'extension du fichier objet sera obj. Le format OMF utilise des directives segment différentes de celles des autres formats objet.

Le segment data (ligne 13) doit être changé en :

Sélectionnez

segment _DATA public align=4 class=DATA use32

Le segment bss (ligne 26) doit être changé en :

Sélectionnez

segment _BSS public align=4 class=BSS use32

Le segment text (ligne 36) doit être changé en :

Sélectionnez

segment _TEXT public align=1 class=CODE use32

De plus, une nouvelle ligne doit être ajoutée avant la ligne 36 :

Sélectionnez

group DGROUP _BSS _DATA

Le compilateur Microsoft C/C++ peut utiliser soit le format OMF, soit le format Win32 pour les fichiers objet (si on lui passe un format OMF, il convertit les informations au format Win32 en interne). Le format Win32 permet de définir les segments comme pour DJGPP et Linux. Utilisez l'option -f win32 pour produire un fichier objet dans ce format. L'extension du fichier objet sera obj.

II-D-3. Assembler le code

La première étape consiste à assembler le code. Depuis la ligne de commande, saisissez :

Sélectionnez

nasm -f format-objet first.asm

où format-objet est soit co , soit elf , soit obj soit win32 selon le compilateur C utilisé (souvenez-vous que le fichier source doit être modifié pour Linux et pour Borland).

II-D-4. Compiler le code C

Compilez le fichier driver.c en utilisant un compilateur C. Pour DJGPP, utilisez :

Sélectionnez

gcc -c driver.c

L'option -c signifie de compiler uniquement, sans essayer de lier. Cette option fonctionne à la fois sur les compilateurs Linux, Borland et Microsoft.

II-D-5. Lier les fichiers objet

L'édition de liens est le procédé qui consiste à combiner le code machine et les données des fichiers objet et des bibliothèques afin de créer un fichier exécutable. Comme nous allons le voir, ce processus est compliqué.

Le code C nécessite la bibliothèque standard du C et un code de démarrage spécial afin de s'exécuter. Il est beaucoup plus simple de laisser le compilateur C appeler l'éditeur de liens avec les paramètres corrects que d'essayer d'appeler l'éditeur de liens directement. Par exemple, pour lier le code du premier programme en utilisant DJGPP, utilisez :

Sélectionnez

gcc -o first driver.o first.o asm_io.o

Cela crée un exécutable appelé first.exe (ou juste first sous Linux).

Avec Borland, on utiliserait :

Sélectionnez

bcc32 first.obj driver.obj asm_io.obj

Borland utilise le nom du premier fichier afin de déterminer le nom de l'exécutable. Donc, dans le cas ci-dessus, le programme s'appellera first.exe.

Il est possible de combiner les étapes de compilation et d'édition de liens.

Par exemple :

Sélectionnez

gcc -o first driver.c first.o asm_io.o

Maintenant gcc compilera driver.c puis liera.

II-D-6. Comprendre un listing assembleur

L'option -l fichier-listing peut être utilisée pour indiquer à nasm de créer un fichier listing avec le nom donné. Ce fichier montre comment le code a été assemblé. Voici comment les lignes 17 et 18 (dans le segment data) apparaissent dans le fichier listing (les numéros de ligne sont dans le

fichier listing ; cependant, notez que les numéros de ligne dans le fichier source peuvent ne pas être les mêmes).

Sélectionnez

48 00000000 456E7465722061206E- prompt1 db "Entrez un nombre : ", 0
49 00000009 756D6265723A2000
50 00000011 456E74657220616E6F- prompt2 db "Entrez un autre nombre : ", 0
51 0000001A 74686572206E756D62-
52 00000023 65723A2000

Les nombres diffèrent sur la version française, car ce ne sont pas les mêmes caractères.

La première colonne de chaque ligne est le numéro de la ligne et la seconde est le déplacement (en hexa) de la donnée dans le segment. La troisième colonne montre les données hexa brutes qui seront stockées. Dans ce cas, les données hexa correspondent aux codes ASCII. Enfin, le texte du fichier source est affiché sur la droite. Les déplacements listés dans la seconde colonne ne sont très probablement pas les déplacements réels auxquels les données seront placées dans le programme terminé. Chaque module peut définir ses propres labels dans le segment de données (et les autres segments également). Lors de l'étape d'édition de liens (voir Section II.D.5), toutes ces définitions de labels de segment de données sont combinées pour ne former qu'un segment de données. Les déplacements finals sont alors calculés par l'éditeur de liens.

Voici une petite portion (lignes 54 à 56 du fichier source) du segment text dans le fichier listing :

Sélectionnez

94 0000002C A1[00000000] mov eax, [input1]
95 00000031 0305[04000000] add eax, [input2]
96 00000037 89C3 mov ebx, eax

La troisième colonne montre le code machine généré par l'assembleur. Souvent le code complet pour une instruction ne peut pas encore être calculé.

Par exemple, ligne 94, le déplacement (ou l'adresse) de input1 n'est pas connu avant que le code ne soit lié. L'assembleur peut calculer l'opcode pour l'instruction mov (qui, d'après le listing, est A1), mais il écrit le déplacement entre crochets, car la valeur exacte ne peut pas encore être calculée. Dans ce cas, un déplacement temporaire de 0 est utilisé, car input1 est au début du segment bss déclaré dans ce fichier. Souvenez-vous que cela ne signifie pas qu'il sera au début du segment bss final du programme. Lorsque le code est lié, l'éditeur de liens insérera le déplacement correct à la place.

D'autres instructions, comme ligne 96, ne font référence à aucun label. Dans ce cas, l'assembleur peut calculer le code machine complet.

Représentations Big et Little Endian

Endian est prononcé comme indian.

Si l'on regarde attentivement la ligne 95, quelque chose semble très bizarre à propos du déplacement entre crochets dans le code machine. Le label input2 est au déplacement 4 (comme défini dans ce fichier) ; cependant, le déplacement qui apparaît en mémoire n'est pas 00000004, mais 04000000.

Pourquoi ? Des processeurs différents stockent les entiers multioctets dans des ordres différents en mémoire. Il y a deux méthodes populaires pour stocker : big endian et little endian. Le big endian est la méthode qui semble la plus naturelle. L'octet le plus fort (i.e. le plus significatif) est stocké en premier, puis le second plus fort, etc. Par exemple, le dword 00000004 serait stocké sous la forme des quatre octets suivants : 00 00 00 04.

Les mainframes IBM, la plupart des processeurs RISC et les processeurs Motorola utilisent tous cette méthode big endian. Cependant, les processeurs de type Intel utilisent la méthode little endian ! Ici, l'octet le moins significatif est stocké en premier. Donc, 00000004 est stocké en mémoire sous la forme 04 00 00 00. Ce format est codé en dur dans le processeur et ne peut pas être changé. Normalement, le programmeur n'a pas besoin de s'inquiéter du format utilisé. Cependant, il y a des circonstances où c'est important.

1. Lorsque des données binaires sont transférées entre différents ordinateurs (soit via des fichiers, soit via un réseau).

2. Lorsque des données binaires sont écrites en mémoire comme un entier multioctet puis relues comme des octets individuels ou vice versa.

Le caractère big ou little endian ne s'applique pas à l'ordre des éléments d'un tableau. Le premier élément d'un tableau est toujours à l'adresse la plus petite. C'est également valable pour les chaînes (qui sont juste des tableaux de caractères). Cependant, le caractère big ou little endian s'applique toujours aux éléments individuels des tableaux.

II-E. Fichier squelette

La Figure 1.7 montre un fichier squelette qui peut être utilisé comme point de départ pour l'écriture de programmes assembleur.

Image non disponible


III

III. Bases du langage assembleur

III-A. Travailler avec les entiers

III-A-1. Représentation des entiers

Les entiers se décomposent en deux catégories : signés et non signés. Les entiers non signés (qui sont positifs) sont représentés d'une manière binaire très intuitive. Le nombre 200 en tant qu'entier non signé sur un octet serait représenté par 11001000 (ou C8 en hexa).

Les entiers signés (qui peuvent être positifs ou négatifs) sont représentés d'une façon plus compliquée. Par exemple, considérons −56. +56 serait représenté par l'octet 00111000. Sur papier, on peut représenter −56 comme −111000, mais comment cela serait-il représenté dans un octet en mémoire de l'ordinateur. Comment serait stocké le signe moins ?

Il y a trois techniques principales qui ont été utilisées pour représenter les entiers signés dans la mémoire de l'ordinateur. Toutes ces méthodes utilisent le bit le plus significatif de l'entier comme bit de signe. Ce bit vaut 0 si le nombre est positif et 1 s'il est négatif.

III-A-1-a. Grandeur signée

La première méthode est la plus simple, elle est appelée grandeur signée.

Elle représente l'entier en deux parties. La première partie est le bit de signe et la seconde est la grandeur entière. Donc 56 serait représenté par l'octet 00111000 (le bit de signe est souligné) et −56 serait 10111000. La valeur d'octet la plus grande serait 01111111 soit +127 et la plus petite valeur sur un octet serait 11111111 soit −127. Pour obtenir l'opposé d'une valeur, on inverse le bit de signe. Cette méthode est intuitive, mais a ses inconvénients. Tout d'abord, il y a deux valeurs possibles pour 0 : +0 (00000000) et −0 (10000000). Comme zéro n'est ni positif ni négatif, ces deux représentations devraient se comporter de la même façon. Cela complique la logique de l'arithmétique pour le processeur. De plus, l'arithmétique générale est également compliquée. Si l'on ajoute 10 à −56, cela doit être transformé en 10 moins 56. Là encore, cela complique la logique du processeur.

III-A-1-b. Complément à 1

La seconde méthode est appelée représentation en complément à un.

Le complément à un d'un nombre est trouvé en inversant chaque bit du nombre (une autre façon de l'obtenir est que la valeur du nouveau bit est 1 − anciennevaleurdubit). Par exemple, le complément à un de 00111000 (+56) est 11000111. Dans la notation en complément à un, calculer le complément à un est équivalent à la négation. Donc 11000111 est la représentation de −56. Notez que le bit de signe a été automatiquement changé par le complément à un et que, comme l'on s'y attendait, appliquer le complément à un deux fois redonne le nombre de départ. Comme pour la première méthode, il y a deux représentations pour 0 : 00000000 (+0) et 11111111 (−0).

L'arithmétique des nombres en complément à 1 est compliquée. Voici une astuce utile pour trouver le complément à 1 d'un nombre en hexadécimal sans repasser en binaire. L'astuce est d'ôter le chiffre hexa de F (ou 15 en décimal). Cette méthode suppose que le nombre de bits dans le nombre est multiple de 4. Voici un exemple : +56 est représenté par 38 en hexa. Pour trouver le complément à 1, ôtez chaque chiffre de F pour obtenir C7 en hexa. Cela concorde avec le résultat ci-dessus.

III-A-1-c. Complément à 2

Les deux premières méthodes décrites ont été utilisées sur les premiers ordinateurs. Les ordinateurs modernes utilisent une troisième méthode appelée représentation en complément à 2. On trouve le complément à 2 d'un nombre en effectuant les deux opérations suivantes :

1. Trouver le complément à un du nombre ;

2. Ajouter 1 au résultat de l'étape 1.

Voici un exemple en utilisant 00111000 (56). Tout d'abord, on calcule le complément à 1 : 11000111. Puis, on ajoute 1 :

11000111

+ 1

11001000

Dans la notation en complément à 2, calculer le complément à 2 est équivalent à trouver l'opposé d'un nombre. Donc, 11001000 est la représentation en complément à 2 de −56. Deux négations devraient reproduire le nombre original. Curieusement le complément à 2 ne remplit pas cette condition. Prenez le complément à 2 de 11001000 en ajoutant 1 au complément à 1.

00110111

+ 1

00111000

Lorsque l'on effectue l'addition dans l'opération de complémentation à 2, l'addition du bit le plus à gauche peut produire une retenue. Cette retenue n'est pas utilisée. Souvenez-vous que toutes les données sur un ordinateur sont d'une taille fixe (en termes de nombre de bits). Ajouter deux octets produit toujours un résultat sur un octet (comme ajouter deux mots donne un mot, etc.) Cette propriété est importante pour la notation en complément à 2. Par exemple, considérons zéro comme un nombre en complément à 2 sur un octet (00000000). Calculer son complément à 2 produit la somme :

11111111

+ 1

c 00000000

où c représente une retenue (plus tard, nous montrerons comment détecter cette retenue, mais elle n'est pas stockée dans le résultat). Donc, en notation en complément à 2, il n'y a qu'un zéro. Cela rend l'arithmétique en complément à 2 plus simple que les méthodes précédentes.

En utilisant la notation en complément à 2, un octet signé peut être utilisé pour représenter les nombres −128 à +127.

Image non disponible

Le Tableau 2.1 montre quelques valeurs choisies. Si 16 bits sont utilisés, les nombres signés −32 768 à +32 767 peuvent être représentés. +32 767 est représenté par 7FFF, −32 768

par 8000, -128 par FF80 et -1 par FFFF. Les nombres en complément à 2 sur 32 bits vont de −2 milliards à +2 milliards environ.

Le processeur n'a aucune idée de ce qu'un octet en particulier (ou un mot ou un double-mot) est supposé représenté. L'assembleur n'a pas le concept de types qu'un langage de plus haut niveau peut avoir. La façon dont les données sont interprétées dépend de l'instruction dans laquelle on les utilise. Que la valeur FF soit considérée comme représentant un −1 signé ou 1 +255 non signé dépend du programmeur. Le langage C définit des types entiers signés et non signés. Cela permet au compilateur C de déterminer les instructions correctes à utiliser avec les données.

III-A-2. Extension de signe

En assembleur, toutes les données ont une taille bien spécifiée. Il n'est pas rare de devoir changer la taille d'une donnée pour l'utiliser avec d'autres. Réduire la taille est le plus simple.

III-A-2-a. Réduire la taille des données

Pour réduire la taille d'une donnée, il suffit d'en retirer les bits les plus significatifs. Voici un exemple trivial :

Sélectionnez

mov ax, 0034h ; ax = 52 (stocké sur 16 bits)
mov cl, al ; cl = les 8 bits de poids faible de ax

Bien sûr, si le nombre ne peut pas être représenté correctement dans une plus petite taille, réduire la taille ne fonctionne pas. Par exemple, si AX valait 0134h (ou 308 en décimal) alors le code ci-dessus mettrait quand même CL à 34h. Cette méthode fonctionne à la fois avec les nombres signés et non signés. Considérons les nombres signés, si AX valait FFFFh (−1 sur un mot), alors CL vaudrait FFh (−1 sur un octet). Cependant, notez que si la valeur dans AX était signée, elle aurait été tronquée et le résultat aurait été faux !

La règle pour les nombres non signés est que tous les bits retirés soient à 0 afin que la conversion soit correcte. La règle pour les nombres signés est que les bits retirés doivent soit tous être des 1 soit tous des 0. De plus, le premier bit à ne pas être retiré doit valoir la même chose que ceux qui l'ont été. Ce bit sera le bit de signe pour la valeur plus petite. Il est important qu'il ait la même valeur que le bit de signe original !

III-A-2-b. Augmenter la taille des données

Augmenter la taille des données est plus compliqué que la réduire. Considérons l'octet hexa FF. S'il est étendu à un mot, quelle valeur aura-t-il ? Cela dépend de la façon dont FF est interprété. Si FF est un octet non signé (255 en décimal), alors le mot doit être 00FF ; cependant, s'il s'agit d'un octet signé (−1 en décimal), alors le mot doit être FFFF.

En général, pour étendre un nombre non signé, on met tous les bits supplémentaires du nouveau nombre à 0. Donc, FF devient 00FF. Cependant, pour étendre un nombre signé, on doit étendre le bit de signe. Cela signifie que les nouveaux bits deviennent des copies du bit de signe. Comme le bit de signe de FF est à 1, les nouveaux bits doivent également être des 1, pour donner FFFF. Si le nombre signé 5A (90 en décimal) était étendu, le résultat serait 005A.

Il y a plusieurs instructions fournies par le 80386 pour étendre les nombres.

Souvenez-vous que l'ordinateur ne sait pas si un nombre est signé ou non. C'est au programmeur d'utiliser l'instruction adéquate. Pour les nombres non signés, on peut placer des 0 dans les bits de poids fort de façon simple en utilisant l'instruction MOV. Par exemple pour étendre l'octet de AL en un mot non signé dans AX :

Sélectionnez

mov ah, 0 ; met les 8 bits de poids fort à 0

Cependant, il n'est pas possible d'utiliser une instruction MOV pour convertir le mot non signé de AX en un double-mot non signé dans EAX. Pourquoi pas ? Il n'y a aucune façon d'accéder aux 16 bits de poids fort de EAX dans un MOV. Le 80386 résout ce problème en fournissant une nouvelle instruction : MOVZX. Cette instruction a deux opérandes. La destination (premier opérande) doit être un registre de 16 ou 32 bits. La source (deuxième opérande) doit être un registre 8 ou 16 bits. L'autre restriction est que la destination doit être plus grande que la source (la plupart des instructions requièrent que la source soit de la même taille que la destination). Voici quelques exemples :

Sélectionnez

movzx eax, ax ; étend ax à eax

movzx eax, al ; étend al à eax

movzx ax, al ; étend al à ax

movzx ebx, ax ; étend ax à ebx

Pour les nombres signés, il n'y a pas de façon simple d'utiliser l'instruction MOV quel que soit le cas. Le 8086 fournit plusieurs instructions pour étendre les nombres signés. L'instruction CBW (Convert Byte to Word, Convertir un Octet en Mot) étend le signe du registre AL dans AX. Les opérandes sont implicites. L'instruction CWD (Convert Word to Double word, Convertir un Mot en Double-mot) étend le signe de AX dans DX:AX. La notation DX:AX signifie qu'il faut considérer les registres DX et AX comme un seul registre 32 bits avec les 16 bits de poids forts dans DX et les bits de poids faible dans AX (souvenez-vous que le 8086 n'avait pas de registre 32 bits du tout !). Le 80386 a apporté plusieurs nouvelles instructions. L'instruction CWDE (Convert Word to Double word Extended, convertir un mot en double mot étendue) étend le signe de AX dans EAX. L'instruction CDQ (Convert Double word to Quad word, Convertir un Double-mot en Quadruple-mot) étend le signe de EAX dans EDX:EAX (64 bits !) Enn, l'instruction MOVSX fonctionne comme MOVZX excepté qu'elle utilise les règles des nombres signés.

III-A-2-c. Application à la programmation en C

L'extension d'entiers signés et non signés a également lieu en C. Les variables en C peuvent être déclarées soit comme signées soit comme non signées (les int sont signés).

Image non disponible

Le C ANSI ne définit pas si le type char est signé ou non, c'est à chaque compilateur de le décider. C'est pourquoi on définit le type explicitement dans la Figure 2.1.

Considérons le code de la Figure 2.1. À la ligne 3, la variable a est étendue en utilisant les règles pour les valeurs non signées (avec MOVZX), mais à la ligne 4, les règles signées sont utilisées pour b (avec MOVSX).

Il existe un bogue de programmation courant en C qui est directement lié à notre sujet. Considérons le code de la Figure 2.2. Le prototype de fgetc()est :

Sélectionnez

int fgetc( FILE * );

On peut se demander pourquoi la fonction renvoie un int puisqu'elle lit des caractères. La raison est qu'elle renvoie normalement un char (étendu à une valeur int en utilisant l'extension de zéro). Cependant, il y a une valeur qu'elle peut retourner qui n'est pas un caractère, EOF. C'est une macro habituellement définie comme valant −1. Donc, fgetc() retourne soit un char étendu à une valeur int (qui ressemble à 000000xx en hexa) soit EOF (qui ressemble à FFFFFFFF en hexa).

Le problème avec le programme de la Figure 2.2 est que fgetc() renvoie un int, mais cette valeur est stockée dans un char. Le C tronquera alors les bits de poids fort pour faire tenir la valeur de l'int dans le char. Le seul problème est que les nombres (en hexa) 000000FF et FFFFFFFF seront tous deux tronqués pour donner l'octet FF. Donc, la boucle while ne peut pas distinguer la lecture de l'octet FF dans le fichier et la fin de ce fichier.

Ce que fait exactement le code dans ce cas change selon que le char est signé ou non. Pourquoi ? Parce que ligne 2, ch est comparé avec EOF. Comme EOF est un int (11), ch sera étendu à un int afin que les deux valeurs comparées soient de la même taille(12). Comme la Figure 2.1 le montrait, le fait qu'une variable soit signée ou non est très important.

Si le char n'est pas signé, FF est étendu et donne 000000FF. Cette valeur est comparée à EOF (FFFFFFFF) et est différente. Donc la boucle ne finit jamais !

Si le char est signé, FF est étendu et donne FFFFFFFF. Les deux valeurs sont alors égales et la boucle se termine. Cependant, comme l'octet FF peut avoir été lu depuis le fichier, la boucle peut se terminer prématurément.

La solution à ce problème est de définir la variable ch comme un int, pas un char. Dans ce cas, aucune troncature ou extension n'est effectuée à la ligne 2. Dans la boucle, il est sans danger de tronquer la valeur puisque ch doit alors être réellement un octet.

III-A-3. Arithmétique en complément à 2

Comme nous l'avons vu plus tôt, l'instruction add effectue une addition et l'instruction sub effectue une soustraction. Deux des bits du registre EFLAGS que ces instructions positionnent sont overow (dépassement de capacité) et carry ag (retenue). Le drapeau d'overow est à 1 si le résultat réel de l'opération est trop grand et ne tient pas dans la destination pour l'arithmétique signée. Le drapeau de retenue est à 1 s'il y a une retenue dans le bit le plus significatif d'une addition d'une soustraction. Donc, il peut être utilisé pour détecter un dépassement de capacité en arithmétique non signée. L'utilisation du drapeau de retenue pour l'arithmétique signée va être vue sous peu. Un des grands avantages du complément à 2 est que les règles pour l'addition et la soustraction sont exactement les mêmes que pour les entiers non signés. Donc, add et sub peuvent être utilisées pour les entiers signés ou non.

002C 44

+ FFFF + (−1)

002B 43

Il y a une retenue de générée, mais elle ne fait pas partie de la réponse.

Il y a deux instructions de multiplication et de division différentes. Tout d'abord, pour multiplier, utilisez les instructions MUL ou IMUL. L'instruction MUL est utilisée pour multiplier les nombres non signés et IMUL est utilisée pour multiplier les entiers signés. Pourquoi deux instructions différentes sont nécessaires ? Les règles pour la multiplication sont différentes pour les nombres non signés et les nombres signés en complément à 2. Pourquoi cela ?

Image non disponible

Considérons la multiplication de l'octet FF avec lui-même donnant un résultat sur un mot. En utilisant la multiplication non signée, il s'agit de 255 fois 255 soit 65 025 (ou FE01 en hexa). En utilisant la multiplication signée, il s'agit de −1 fois −1 soit 1 (ou 0001 en hexa).

Il y a plusieurs formes pour les instructions de multiplication. La plus ancienne ressemble à cela :

Sélectionnez

mul source

La source est soit un registre soit une référence mémoire. Cela ne peut pas être une valeur immédiate. Le type exact de la multiplication dépend de la taille de l'opérande source. Si l'opérande est un octet, elle est multipliée par l'octet situé dans le registre AL et le résultat est stocké dans les 16 bits de AX. Si la source fait 16 bits, elle est multipliée par le mot situé dans AX et le résultat 32 bits est stocké dans DX:AX. Si la source fait 32 bits, elle est multipliée par EAX et le résultat 64 bits est stocké dans EDX:EAX.

L'instruction IMUL a les mêmes formats que MUL, mais ajoute également d'autres formats d'instruction. Il y a des formats à deux et à trois opérandes :

Sélectionnez

imul dest, source1

imul dest, source1, source2

Le Tableau 2.2 montre les combinaisons possibles.

Les deux opérateurs de division sont DIV et IDIV. Ils effectuent respectivement une division entière non signée et une division entière signée. Le format général est :

Sélectionnez

div source

Si la source est sur 8 bits, alors AX est divisé par l'opérande. Le quotient est stocké dans AL et le reste dans AH. Si la source est sur 16 bits, alors DX:AX est divisé par l'opérande. Le quotient est stocké dans AX et le reste dans DX. Si la source est sur 32 bits, alors EDX:EAX est divisé par l'opérande, le quotient est stocké dans EAX et le reste dans EDX. L'instruction IDIV fonctionne de la même façon. Il n'y a pas d'instructions IDIV spéciales comme pour IMUL. Si le quotient est trop grand pour tenir dans son registre ou que le diviseur vaut 0, le programme et interrompu et se termine. Une erreur courante est d'oublier d'initialiser DX ou EDX avant la division.

L'instruction NEG inverse son opérande en calculant son complément à 2. L'opérande peut être n'importe quel registre ou emplacement mémoire sur 8 bits, 16 bits, or 32 bits.

III-A-4. Programme exemple

math.asm

Sélectionnez

1 %include "asm_io.inc"
2 segment .data ; Chaines affichees
3 prompt db "Entrez un nombre : ", 0
4 square_msg db "Le carre de l'entree vaut ", 0
5 cube_msg db "Le cube de l'entree vaut ", 0
6 cube25_msg db "Le cube de l'entree fois 25 vaut ", 0
7 quot_msg db "Le quotient de cube/100 vaut ", 0
8 rem_msg db "Le reste de cube/100 vaut ", 0
9 neg_msg db "La négation du reste vaut ", 0
10
11 segment .bss
12 input resd 1
13
14 segment .text
15 global _asm_main
16 _asm_main :
17 enter 0,0 ; routine d'initialisation
18 pusha
19
20 mov eax, prompt
21 call print_string
22
23 call read_int
24 mov [input], eax
25
26 imul eax ; edx:eax = eax * eax
27 mov ebx, eax ; sauvegarde le résultat dans ebx
28 mov eax, square_msg
29 call print_string
30 mov eax, ebx
31 call print_int
32 call print_nl
33
34 mov ebx, eax
35 imul ebx, [input] ; ebx *= [entree]
36 mov eax, cube_msg
37 call print_string
38 mov eax, ebx
39 call print_int
40 call print_nl
41
42 imul ecx, ebx, 25 ; ecx = ebx*25
43 mov eax, cube25_msg
44 call print_string
45 mov eax, ecx
46 call print_int
47 call print_nl
48
49 mov eax, ebx
50 cdq ; initialise edx avec extension de signe
51 mov ecx, 100 ; on ne peut pas diviser par une valeur immédiate
52 idiv ecx ; edx:eax / ecx
53 mov ecx, eax ; sauvegarde le résultat dans ecx
54 mov eax, quot_msg
55 call print_string
56 mov eax, ecx
57 call print_int
58 call print_nl
59 mov eax, rem_msg
60 call print_string
61 mov eax, edx
62 call print_int
63 call print_nl
64
65 neg edx ; inverse le reste
66 mov eax, neg_msg
67 call print_string
68 mov eax, edx
69 call print_int
70 call print_nl
71
72 popa
73 mov eax, 0 ; retour au C
74 leave
75 ret

III-A-5. Arithmétique en précision étendue

Le langage assembleur fournit également des instructions qui permettent d'effectuer des additions et des soustractions sur des nombres plus grands que des doubles mots. Ces instructions utilisent le drapeau de retenue. Comme nous l'avons dit plus haut, les instructions ADD et SUB modifient le drapeau de retenue si une retenue est générée. Cette information stockée dans le drapeau de retenue peut être utilisée pour additionner ou soustraire de grands nombres en morcelant l'opération en doubles-mots (ou plus petit).

Les instructions ADC et SBB utilisent les informations données par le drapeau de retenue. L'instruction ADC effectue l'opération suivante :

opérande1 = opérande1 + drapeau de retenue + opérande2

L'instruction SBB effectue :

opérande1 = opérande1 - drapeau de retenue - opérande2

Comment sont-elles utilisées ? Considérons la somme d'entiers 64 bits dans EDX:EAX et EBX:ECX. Le code suivant stockerait la somme dans EDX:EAX :

Sélectionnez

1 add eax, ecx ; additionne les 32 bits de poids faible

2 adc edx, ebx ; additionne les 32 bits de poids fort et la retenue

La soustraction est très similaire. Le code suivant soustrait EBX:ECX de EDX:EAX :

Sélectionnez

1 sub eax, ecx ; soustrait les 32 bits de poids faible

2 sbb edx, ebx ; soustrait les 32 bits de poids fort et la retenue

Pour les nombres vraiment grands, une boucle peut être utilisée (voir Section III.B). Pour une boucle de somme, il serait pratique d'utiliser l'instruction ADC pour toutes les itérations (au lieu de toutes sauf la première).

Cela peut être fait en utilisant l'instruction CLC (CLear Carry) juste avant que la boucle ne commence pour initialiser le drapeau de retenue à 0. Si le drapeau de retenue vaut 0, il n'y a pas de différence entre les instructions ADD et ADC. La même idée peut être utilisée pour la soustraction.

III-B. Structures de contrôle

Les langages de haut niveau fournissent des structures de contrôle de haut niveau (p.e., les instructions if et while) qui contrôlent le flot d'exécution. Le langage assembleur ne fournit pas de structures de contrôle aussi complexes. Il utilise à la place l'infâme goto et l'utiliser de manière inappropriée peut conduire à du code spaghetti ! Cependant, il est possible d'écrire des programmes structurés en assembleur. La procédure de base est de concevoir la logique du programme en utilisant les structures de contrôle de haut niveau habituelles et de traduire cette conception en langage assembleur (comme le ferait un compilateur).

III-B-1. Comparaison

Les structures de contrôle décident ce qu'il faut faire en comparant des données. En assembleur, le résultat d'une comparaison est stocké dans le registre FLAGS pour être utilisé plus tard. Le 80x86 fournit l'instruction CMP pour effectuer des comparaisons. Le registre FLAGS est positionné selon la différence entre les deux opérandes de l'instruction CMP. Les opérandes sont soustraits et le registre FLAGS est positionné selon le résultat, mais ce résultat n'est stocké nulle part. Si vous avez besoin du résultat, utilisez l'instruction SUB au lieu de CMP.

Pour les entiers non signés, il y a deux drapeaux (bits dans le registre FLAGS) qui sont importants : les drapeaux zéro (ZF) et retenue (CF). Le drapeau zero est allumé (1) si le résultat de la différence serait 0. Considérons une comparaison comme :

Sélectionnez

cmp vleft, vright

La différence vleft - vright est calculée et les drapeaux sont positionnés en fonction. Si la différence calculée par CMP vaut 0, vleft = vright, alors ZF est allumé(i.e. 1) et CF est éteint (i.e. 0). Si vleft > vright, ZF est éteint et CF également. Si vleft < vright, alors ZF est éteint et CF est allumé (retenue).

Pour les entiers signés, il y a trois drapeaux importants : le drapeau zéro (ZF), le drapeau d'overow (OF) et le drapeau de signe (SF). Le drapeau d'overow est allumé si le résultat d'une opération dépasse la capacité (de trop ou de trop peu). Le drapeau de signe est allumé si le résultat d'une opération est négatif. Si vleft = vright, ZF est allumé (comme pour les entiers non signés). Si vleft > vright, ZF est éteint et SF = OF. Si vleft < vright, ZF est éteint et SF 6= OF.

N'oubliez pas que d'autres instructions peuvent aussi changer le registre FLAGS, pas seulement CMP.

Pourquoi SF = OF si vleft > vright ? S'il n'y
a pas d'overow, alors la différence aura la valeur correcte et doit être positive ou nulle. Donc, SF = OF = 0. Par contre, s'il y a un overow, la différence n'aura pas la valeur correcte (et sera en fait négative).
Donc, SF = OF = 1.

III-B-2. Instructions de branchement

Les instructions de branchement peuvent transférer l'exécution à n'importe quel point du programme. En d'autres termes, elles agissent comme un goto. Il y a deux types de branchements : conditionnel et inconditionnel. Un branchement inconditionnel est exactement comme un goto, il effectue toujours le branchement. Un branchement conditionnel peut effectuer le branchement ou non selon les drapeaux du registre FLAGS. Si un branchement conditionnel n'effectue pas le branchement, le contrôle passe à l'instruction suivante.

L'instruction JMP (abréviation de jump) effectue les branchements inconditionnels.

Son seul argument est habituellement l'étiquette de code de l'instruction à laquelle se brancher. L'assembleur ou l'éditeur de liens remplacera l'étiquette par l'adresse correcte de l'instruction. C'est une autre des opérations pénibles que l'assembleur fait pour rendre la vie du programmeur plus simple. Il est important de réaliser que l'instruction immédiatement après l'instruction JMP ne sera jamais exécutée à moins qu'une autre instruction ne se branche dessus !

Il y a plusieurs variantes de l'instruction de saut :

SHORT ce saut est très limité en portée. Il ne peut bouger que de plus ou moins 128 octets en mémoire. L'avantage de ce type de saut est qu'il utilise moins de mémoire que les autres. Il utilise un seul octet signé pour stocker le déplacement du saut. Le déplacement est le nombre d'octets à sauter vers l'avant ou vers l'arrière (le déplacement est ajouté à EIP). Pour spécifier un saut court, utilisez le mot-clé SHORT immédiatement avant l'étiquette dans l'instruction JMP ;

NEAR ce saut est le saut par défaut, que ce soit pour les branchements inconditionnels ou conditionnels, il peut être utilisé pour sauter vers n'importe quel emplacement dans un segment. En fait, le 80386 supporte deux types de sauts proches. L'un utilise deux octets pour le déplacement. Cela permet de se déplacer en avant ou en arrière d'environ 32 000 octets. L'autre type utilise quatre octets pour le déplacement, ce qui, bien sûr, permet de se déplacer n'importe où dans le segment.

Le type de saut à deux octets peut être spécifié en plaçant le mot-clé WORD avant l'étiquette dans l'instruction JMP.

FAR ce saut permet de passer le contrôle à un autre segment de code. C'est une chose très rare en mode protégé 386.

Les étiquettes de code valides suivent les mêmes règles que les étiquettes de données. Les étiquettes de code sont définies en les plaçant dans le segment de code au début de l'instruction qu'ils étiquettent. Deux points sont placés à la fin de l'étiquette lors de sa définition. Ces deux points ne font pas partie du nom.

Image non disponible

Il y a beaucoup d'instructions de branchement conditionnel différentes. Elles prennent toutes une étiquette de code comme seul opérande. Les plus simples se contentent de regarder un drapeau dans le registre FLAGS pour déterminer si elles doivent sauter ou non. Voyez le Tableau 2.3 pour une liste de ces instructions (PF est le drapeau de parité qui indique si le nombre de bits dans les 8 bits de poids faible du résultat est pair ou impair).

Le pseudo-code suivant :

Sélectionnez

if ( EAX == 0 )

EBX = 1;

else

EBX = 2;

peut être écrit de cette façon en assembleur :

Sélectionnez

1 cmp eax, 0 ; positionne les drapeaux (ZF allumé si eax - 0 = 0)

2 jz thenblock ; si ZF est allumé, branchement vers thenblock

3 mov ebx, 2 ; partie ELSE du IF

4 jmp next ; saute par dessus la partie THEN du IF

5 thenblock :

6 mov ebx, 1 ; partie THEN du IF

7 next :

Les autres comparaisons ne sont pas si simples si l'on se contente des branchements conditionnels du Tableau 2.3. Pour illustrer, considérons le pseudo-code suivant :

Sélectionnez

if ( EAX >= 5 )

EBX = 1;

else

EBX = 2;

Si EAX est plus grand ou égal à 5, ZF peut être allumé ou non et SF sera égal à OF. Voici le code assembleur qui teste ces conditions (en supposant que EAX est signé) :

Sélectionnez

1 cmp eax, 5
2 js signon ; goto signon si SF = 1
3 jo elseblock ; goto elseblock si OF = 1 et SF = 0
4 jmp thenblock ; goto thenblock si SF = 0 et OF = 0
5 signon:
6 jo thenblock ; goto thenblock si SF = 1 et OF = 1
7 elseblock :
8 mov ebx, 2
9 jmp next
10 thenblock:
11 mov ebx, 1
12 next:

Le code ci-dessus est très confus. Heureusement, le 80x86 fournit des instructions de branchement additionnelles pour rendre ce type de test beaucoup plus simple. Il y a des versions signées et non signées de chacune.

Image non disponible

Le Tableau 2.4 montre ces instructions. Les branchements égal et non égal (JE et JNE) sont les mêmes pour les entiers signés et non signés (en fait, JE et JNE sont réellement identiques à JZ et JNZ, respectivement). Chacune des autres instructions de branchement a deux synonymes. Par exemple, regardez JL (jump less than, saut si inférieur à) et JNGE (jump not greater than or equal to, saut si non plus grand ou égal). Ce sont les mêmes instructions, car :

x < y => not(x y)

Les branchements non signés utilisent A pour above (au-dessus) et B pour below (en dessous) à la place de L et G.

En utilisant ces nouvelles instructions de branchement, le pseudo-code ci-dessus peut être traduit en assembleur plus facilement.

Sélectionnez

1 cmp eax, 5
2 jge thenblock
3 mov ebx, 2
4 jmp next
5 thenblock:
6 mov ebx, 1
7 next:

III-B-3. Les instructions de boucle

Le 80x86 fournit plusieurs instructions destinées à implémenter des boucles de style for. Chacune de ces instructions prend une étiquette de code comme seul opérande.

LOOP décrémente ECX, si ECX 6= 0, saute vers l'étiquette.

LOOPE, LOOPZ décrémente ECX (le registre FLAGS n'est pas modifié), si ECX 6= 0 et ZF = 1, saute.

LOOPNE, LOOPNZ décrémente ECX (FLAGS inchangé), si ECX 6= 0 et ZF = 0, saute.

Les deux dernières instructions de boucle sont utiles pour les boucles de recherche séquentielle. Le pseudo-code suivant :

Sélectionnez

sum = 0;

for ( i=10; i >0; i&#8722;&#8722; )

sum += i;

peut être traduit en assembleur de la façon suivante ;

Sélectionnez

1 mov eax, 0 ; eax est sum
2 mov ecx, 10 ; ecx est i
3 loop_start:
4 add eax, ecx
5 loop loop_start

III-C. Traduire les structures de contrôle standard

Cette section décrit comment les structures de contrôle standard des langages de haut niveau peuvent être implémentées en langage assembleur.

III-C-1. Instructions if

Le pseudo-code suivant :

Sélectionnez

if ( condition )
then_block;
else
else_block;

peut être implémenté de cette façon :

Sélectionnez

1 ; code pour positionner FLAGS
2 jxx else_block ; choisir xx afin de sauter si la condition est fausse
3 ; code pour le bloc then
4 jmp endif
5 else_block :
6 ; code pour le bloc else
7 endif :

S'il n'y a pas de else, le branchement else_block peut être remplacé par un branchement à endif.

Sélectionnez

1 ; code pour positionner FLAGS
2 jxx endif ; choisir xx afin de sauter si la condition est fausse
3 ; code pour le bloc then
4 endif :

III-C-2. Boucles while

La boucle while est une boucle avec un test au début :

Sélectionnez

while( condition ) {

corps de la boucle;

}

Cela peut être traduit en :

Sélectionnez

1 while :
2 ; code pour positionner FLAGS selon la condition
3 jxx endwhile ; choisir xx pour sauter si la condition est fausse
4 ; body of loop
5 jmp while
6 endwhile :

III-C-3. Boucles do while

La boucle do while est une boucle avec un test à la fin :

Sélectionnez

do {

corps de la boucle;

} while( condition );

Cela peut être traduit en :

Sélectionnez

1 do :

2 ; corps de la boucle

3 ; code pour positionner FLAGS selon la condition

4 jxx do ; choisir xx pour se brancher si la condition est vraie

III-D. Exemple : trouver des nombres premiers

Cette section détaille un programme qui trouve des nombres premiers.

Rappelez-vous que les nombres premiers ne sont divisibles que par 1 et par eux-mêmes. Il n'y a pas de formule pour les rechercher. La méthode de base qu'utilise ce programme est de rechercher les diviseurs de tous les nombres impairs (13) sous une limite donnée. Si aucun facteur ne peut être trouvé pour un nombre impair, il est premier. Voici l'algorithme de base écrit en C.

Sélectionnez

1 unsigned guess; / candidat courant pour être premier /

2 unsigned factor ; / diviseur possible de guess /

3 unsigned limit ; / rechercher les nombres premiers jusqu'à cette valeur /

4

5  printf ("Rechercher les nombres premiers jusqu'a : ");

6 scanf("%u", &limit);

7 printf ("2\n"); / traite les deux premiers nombres premiers comme /

8 printf ("3\n"); / des cas spéciaux /

9 guess = 5; / candidat initial /

10 while ( guess <= limit ) {

11 / recherche un diviseur du candidat /

12 factor = 3;

13 while ( factor factor < guess &&

14 guess % factor != 0 )

15 factor += 2;

16 if ( guess % factor != 0 )

17 printf ("%d\n", guess);

18 guess += 2; / ne regarder que les nombres impairs /

19 }

Voici la version assembleur :

prime.asm

Sélectionnez

1 %include "asm_io.inc"
2 segment .data
3 Message db "Rechercher les nombres premiers jusqu'a : ", 0
4
5  segment .bss
6 Limit resd 1 ; rechercher les nombres premiers jusqu'à cette limite
7 Guess resd 1 ; candidat courant pour être premier
8
9  segment .text
10 global _asm_main
11 _asm_main :
12 enter 0,0 ; routine d'initialisation
13 pusha
14
15 mov eax, Message
16 call print_string
17 call read_int ; scanf("%u", &limit);
18 mov [Limit], eax
19
20 mov eax, 2 ; printf("2\n");
21 call print_int
22 call print_nl
23 mov eax, 3 ; printf("3\n");
24 call print_int
25 call print_nl
26
27 mov dword [Guess], 5 ; Guess = 5;
28 while_limit : ; while ( Guess <= Limit )
29 mov eax,[Guess]
30 cmp eax, [Limit]
31 jnbe end_while_limit ; on utilise jnbe, car les nombres ne sont pas signes
32
33 mov ebx, 3 ; ebx est factor = 3;
34 while_factor:
35 mov eax,ebx
36 mul eax ; edx:eax = eax*eax
37 jo end_while_factor ; si la réponse ne tient pas dans eax seul
38 cmp eax, [Guess]
39 jnb end_while_factor ; si !(factor*factor < guess)
40 mov eax,[Guess]
41 mov edx,0
42 div ebx ; edx = edx:eax % ebx
43 cmp edx, 0
44 je end_while_factor ; si !(guess % factor != 0)
45
46 add ebx,2 ; factor += 2;
47 jmp while_factor
48 end_while_factor:
49 je end_if ; si !(guess % factor != 0)
50 mov eax,[Guess] ; printf("%u\n")
51 call print_int
52 call print_nl
53 end_if :
54 add dword [Guess], 2 ; guess += 2
55 jmp while_limit
56 end_while_limit:
57
58 popa
59 mov eax, 0 ; retour au C
60 leave
61 ret


IV. Opérations sur les bits

IV-A. Opérations de décalage

Le langage assembleur permet au programmeur de manipuler individuellement les bits des données. Une opération courante sur les bits est appelée décalage. Une opération de décalage déplace la position des bits d'une donnée. Les décalages peuvent être soit vers la gauche (i.e. vers les bits les plus significatifs) soit vers la droite (les bits les moins significatifs).

IV-A-1. Décalages logiques

Le décalage logique est le type le plus simple de décalage. Il décale d'une manière très simple. La Figure 3.1 montre l'exemple du décalage d'un nombre sur un octet.

Image non disponible

Notez que les nouveaux bits sont toujours à 0. Les instructions SHL et SHR sont respectivement utilisées pour décaler à gauche et à droite. Ces instructions permettent de décaler de n'importe quel nombre de positions.

Le nombre de positions à décaler peut soit être une constante, soit être stocké dans le registre CL. Le dernier bit décalé de la donnée est stocké dans le drapeau de retenue. Voici quelques exemples :

Sélectionnez

1 mov ax, 0C123H

2 shl ax, 1 ; décale d'1 bit à gauche, ax = 8246H, CF = 1

3 shr ax, 1 ; décale d'1 bit à droite, ax = 4123H, CF = 0

4 shr ax, 1 ; décale d'1 bit à droite, ax = 2091H, CF = 1

5 mov ax, 0C123H

6 shl ax, 2 ; décale de 2 bits à gauche, ax = 048CH, CF = 1

7 mov cl, 3

8 shr ax, cl ; décale de 3 bits à droite, ax = 0091H, CF = 1

IV-A-2. Utilisation des décalages

La multiplication et la division rapides sont les utilisations les plus courantes des opérations de décalage. Rappelez-vous que dans le système décimal, la multiplication et la division par une puissance de dix sont simples, on décale simplement les chiffres. C'est également vrai pour les puissances de deux en binaire. Par exemple, pour multiplier par deux le nombre binaire 10112 (ou 11 en décimal), décalez d'un cran vers la gauche pour obtenir 101102 (ou 22). Le quotient d'une division par une puissance de 2 est le résultat d'un décalage à droite. Pour diviser par 2, utilisez un décalage d'une position à droite ; pour diviser par 4 (22), décalez à droite de deux positions ; pour diviser par 8 (23), décalez de 3 positions vers la droite, etc. Les instructions de décalage sont très basiques et sont beaucoup plus rapides que les instructions MUL et DIV correspondantes !

En fait, les décalages logiques peuvent être utilisés pour multiplier ou diviser des valeurs non signées. Ils ne fonctionnent généralement pas pour les valeurs signées. Considérons la valeur sur deux octets FFFF (−1 signé). Si on lui applique un décalage logique d'une position vers la droite, le résultat est 7FFF ce qui fait +32 767 ! Un autre type de décalage doit être utilisé pour les valeurs signées.

IV-A-3. Décalages arithmétiques

Ces décalages sont conçus pour permettre à des nombres signés d'être rapidement multipliés et divisés par des puissances de 2. Ils assurent que le bit de signe est traité correctement.

SAL Shift Arithmetic Left (décalage arithmétique à gauche) - Cette instruction est juste un synonyme pour SHL. Elle est assemblée pour donner exactement le même code machine que SHL. Tant que le bit de signe n'est pas changé par le décalage, le résultat sera correct.

SAR Shift Arithmetic Right (décalage arithmétique à droite) - C'est une nouvelle instruction qui ne décale pas le bit de signe (i.e. le msb) de son opérande. Les autres bits sont décalés normalement sauf que les nouveaux bits qui entrent par la gauche sont des copies du bit de signe (c'est-à-dire que si le bit de signe est à 1, les nouveaux bits sont également à 1). Donc, si un octet est décalé avec cette instruction, seuls les 7 bits de poids faible sont décalés. Comme pour les autres décalages, le dernier bit sorti est stocké dans le drapeau de retenue.

Sélectionnez

1 mov ax, 0C123H

2 sal ax, 1 ; ax = 8246H, CF = 1

3 sal ax, 1 ; ax = 048CH, CF = 1

4 sar ax, 2 ; ax = 0123H, CF = 0

IV-A-4. Décalages circulaires

Les instructions de décalage circulaire fonctionnent comme les décalages logiques excepté que les bits perdus à un bout sont réintégrés à l'autre. Donc, la donnée est traitée comme s'il s'agissait d'une structure circulaire. Les deux instructions de rotation les plus simples sont ROL et ROR qui effectuent des rotations à gauche et à droite, respectivement. Comme pour les autres décalages, ceux-ci laissent une copie du dernier bit sorti dans le drapeau de retenue.

Sélectionnez

1 mov ax, 0C123H

2 rol ax, 1 ; ax = 8247H, CF = 1

3 rol ax, 1 ; ax = 048FH, CF = 1

4 rol ax, 1 ; ax = 091EH, CF = 0

5 ror ax, 2 ; ax = 8247H, CF = 1

6 ror ax, 1 ; ax = C123H, CF = 1

Il y a deux instructions de rotation supplémentaires qui décalent les bits de la donnée et le drapeau de retenue appelés RCL et RCR. Par exemple, si on applique au registre AX une rotation avec ces instructions, elle est appliquée aux 17 bits constitués de AX et du drapeau de retenue.

Sélectionnez

1 mov ax, 0C123H

2 clc ; eteint le drapeau de retenue (CF = 0)

3 rcl ax, 1 ; ax = 8246H, CF = 1

4 rcl ax, 1 ; ax = 048DH, CF = 1

5 rcl ax, 1 ; ax = 091BH, CF = 0

6 rcr ax, 2 ; ax = 8246H, CF = 1

7 rcr ax, 1 ; ax = C123H, CF = 0

IV-A-5. Application simple

Voici un extrait de code qui compte le nombre de bits qui sont allumés (i.e. à 1) dans le registre EAX.

Sélectionnez

1 mov bl, 0 ; bl contiendra le nombre de bits ALLUMES
2 mov ecx, 32 ; ecx est le compteur de boucle
3 count_loop:
4 shl eax, 1 ; decale un bit dans le drapeau de retenue
5 jnc skip_inc ; si CF == 0, goto skip_inc
6 inc bl
7 skip_inc :
8 loop count_loop

Le code ci-dessus détruit la valeur originale de EAX (EAX vaut zéro à la fin de la boucle). Si l'on voulait conserver la valeur de EAX, la ligne 4 pourrait être remplacée par rol eax, 1.

IV-B. Opérations booléennes niveau bit

Image non disponible

Il y a quatre opérateurs booléens courants : AND, OR, XOR et NOT. Une table de vérité montre le résultat de chaque opération pour chaque valeur possible de ses opérandes.

IV-B-1. L'opération ET

Le résultat d'un ET sur deux bits vaut 1 uniquement si les deux bits sont à 1, sinon, le résultat vaut 0, comme le montre la table de vérité du Tableau 3.1.

Les processeurs supportent ces opérations comme des instructions agissant de façon indépendante sur tous les bits de la donnée en parallèle. Par exemple, si on applique un ET au contenu de AL et BL, l'opération ET de base est appliquée à chacune des 8 paires de bits correspondantes dans les deux registres, comme le montre la Figure 3.2. Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H

2 and ax, 82F6H ; ax = 8022H

IV-B-2. L'opération OU

Image non disponible

Le OU de 2 bits vaut 0 uniquement si les deux bits valent 0, sinon le résultat vaut 1 comme le montre la table de vérité du Tableau 3.2. Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H
2 or ax, 0E831H ; ax = E933H

IV-B-3. L'opération XOR

Le OU exclusif de deux bits vaut 0 uniquement si les deux bits sont égaux, sinon, le résultat vaut 1 comme le montre la table de vérité du Tableau 3.3.

Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H

2 xor ax, 0E831H ; ax = 2912H

IV-B-4. L'opération NOT

Image non disponible

L'opération NOT est une opération unaire (i.e. elle agit sur un seul opérande, pas deux comme les opérations binaires de type ET). Le NOT d'un bit est la valeur opposée du bit comme le montre la table de vérité du Tableau 3.4. Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H

2 not ax ; ax = 3EDCH

Notez que le NOT donne le complément à un. Contrairement aux autres opérations niveau bit, l'instruction NOT ne change aucun des bits du registre FLAGS.

IV-B-5. L'instruction TEST

L'instruction TEST effectue une opération AND, mais ne stocke pas le résultat. Elle positionne le registre FLAGS selon ce que ce dernier aurait été après un AND (comme l'instruction CMP qui effectue une soustraction, mais ne fait que positionner FLAGS). Par exemple, si le résultat était zéro, ZF serait allumé.

IV-B-6. Utilisation des opérations sur les bits

Les opérations sur les bits sont très utiles pour manipuler individuellement les bits d'une donnée sans modifier les autres bits. Le Tableau 3.5 montre trois utilisations courantes de ces opérations.

Image non disponible

Voici un exemple de code, implémentant ces idées.

Sélectionnez

1 mov ax, 0C123H

2 or ax, 8 ; allumer le bit 3, ax = C12BH

3 and ax, 0FFDFH ; éteindre le bit 5, ax = C10BH

4 xor ax, 8000H ; inverser le bit 31, ax = 410BH

5 or ax, 0F00H ; allumer un quadruplet, ax = 4F0BH

6 and ax, 0FFF0H ; éteindre un quadruplet, ax = 4F00H

7 xor ax, 0F00FH ; inverser des quadruplets, ax = BF0FH

8 xor ax, 0FFFFH ; complément à 1, ax = 40F0H

L'opération ET peut aussi être utilisée pour trouver le reste d'une division par une puissance de deux. Pour trouver le reste d'une division par 2i, effectuez un ET sur le nombre avec un masque valant 2i − 1. Ce masque contiendra des 1 du bit 0 au bit i−1. Ce sont tout simplement ces bits qui correspondent au reste. Le résultat du ET conservera ces bits et mettra les autres à zéro. Voici un extrait de code qui trouve le quotient et le reste de la division de 100 par 16.

Sélectionnez

1 mov eax, 100 ; 100 = 64H

2 mov ebx, 0000000FH ; masque = 16 - 1 = 15 ou F

3 and ebx, eax ; ebx = reste = 4

En utilisant le registre CL il est possible de modifier n'importe quel(s) bit(s) d'une donnée. Voici un exemple qui allume un bit de EAX. Le numéro du bit à allumer est stocké dans BH.

Sélectionnez

1 mov cl, bh ; tout d'abord, construire le nombre pour le OU

2 mov ebx, 1

3 shl ebx, cl ; décalage à gauche cl fois

4 or eax, ebx ; allume le bit

Éteindre un bit est un tout petit peut plus dur.

Sélectionnez

1 mov cl, bh ; tout d'abord, construire le nombre pour le ET

2 mov ebx, 1

3 shl ebx, cl ; décalage à gauche cl fois

4 not ebx ; inverse les bits

5 and eax, ebx ; éteint le bit

Le code pour inverser un bit est laissé en exercice au lecteur.

Il n'est pas rare de voir l'instruction déroutante suivante dans un programme 80x86 :

Sélectionnez

xor eax, eax ; eax = 0

Un nombre auquel on applique un XOR avec lui-même donne toujours zéro.

Cette instruction est utilisée, car son code machine est plus petit que l'instruction MOV correspondante.

IV-C. Éviter les branchements conditionnels

Les processeurs modernes utilisent des techniques très sophistiquées pour exécuter le code le plus rapidement possible. Une technique répandue est appelée exécution spéculative. Cette technique utilise les possibilités de traitement en parallèle du processeur pour exécuter plusieurs instructions à la fois. Les branchements conditionnels posent un problème à ce type de fonctionnement.

Le processeur, en général, ne sait pas si le branchement sera effectué ou pas. Selon qu'il est effectué ou non, un ensemble d'instructions différent sera exécuté. Les processeurs essaient de prévoir si le branchement sera effectué. Si la prévision est mauvaise, le processeur a perdu son temps en exécutant le mauvais code.

Une façon d'éviter ce problème est d'éviter d'utiliser les branchements conditionnels lorsque c'est possible. Le code d'exemple de III.A.5 fournit un exemple simple de la façon de le faire. Dans l'exemple précédent, les bits allumés du registre EAX sont décomptés. Il utilise un branchement pour éviter l'instruction INC. La Figure 3.3 montre comment le branchement peut être retiré en utilisant l'instruction ADC pour ajouter directement le drapeau de retenue.

Image non disponible

Les instructions SETxx fournissent un moyen de retirer les branchements dans certains cas. Ces instructions positionnent la valeur d'un registre ou d'un emplacement mémoire d'un octet à zéro ou à un selon l'état du registre FLAGS. Les caractères après SET sont les mêmes que pour les branchements conditionnels. Si la condition correspondante au SETxx est vraie, le résultat stocké est un, s'il est faux, zéro est stocké. Par exemple :

Sélectionnez

setz al ; AL = 1 si ZF est allume, sinon 0

En utilisant ces instructions, il est possible de développer des techniques ingénieuses qui calculent des valeurs sans branchement.

Par exemple, considérons le problème de la recherche de la plus grande de deux valeurs. L'approche standard pour résoudre ce problème serait d'utiliser un CMP et un branchement conditionnel pour déterminer la valeur la plus grande. Le programme exemple ci-dessous montre comment le maximum peut être trouvé sans utiliser aucun branchement.

Sélectionnez

1 ; file : max.asm
2 %include "asm_io.inc"
3 segment .data
4
5  message1 db "Entrez un nombre : ",0
6 message2 db "Entrez un autre nombre : ", 0
7 message3 db "Le plus grand nombre est : ", 0
8
9  segment .bss
10
11 input1 resd 1 ; premier nombre entre
12
13 segment .text
14 global _asm_main
15 _asm_main :
16 enter 0,0 ; routine d'initialisation
17 pusha
18
19 mov eax, message1 ; affichage du premier message
20 call print_string
21 call read_int ; saisie du premier nombre
22 mov [input1], eax
23
24 mov eax, message2 ; affichage du second message
25 call print_string
26 call read_int ; saisie du second nombre (dans eax)
27
28 xor ebx, ebx ; ebx = 0
29 cmp eax, [input1] ; compare le premier et le second nombre
30 setg bl ; ebx = (input2 > input1) ? 1 : 0
31 neg ebx ; ebx = (input2 > input1) ? 0xFFFFFFFF : 0
32 mov ecx, ebx ; ecx = (input2 > input1) ? 0xFFFFFFFF : 0
33 and ecx, eax ; ecx = (input2 > input1) ? input2 : 0
34 not ebx ; ebx = (input2 > input1) ? 0 : 0xFFFFFFFF
35 and ebx, [input1] ; ebx = (input2 > input1) ? 0 : input1
36 or ecx, ebx ; ecx = (input2 > input1) ? input2 : input1
37
38 mov eax, message3 ; affichage du résultat
39 call print_string
40 mov eax, ecx
41 call print_int
42 call print_nl
43
44 popa
45 mov eax, 0 ; retour au C
46 leave
47 ret

L'astuce est de créer un masque de bits qui peut être utilisé pour sélectionner la valeur correcte pour le maximum. L'instruction SETG à la ligne 30 positionne BL à 1 si la seconde saisie est la plus grande ou à 0 sinon. Ce n'est pas le masque de bits désiré.

Pour créer le masque nécessaire, la ligne 31 utilise l'instruction NEG sur le registre EBX (notez que EBX a été positionné à 0 précédemment). Si EBX vaut 0, cela ne fait rien ; cependant, si EBX vaut 1, le résultat est la représentation en complément à 2 de -1 soit 0xFFFFFFFF. C'est exactement le masque de bits désiré.

Le code restant utilise ce masque de bits pour sélectionner la saisie correspondant au plus

grand nombre.

Une autre astuce est d'utiliser l'instruction DEC. Dans le code ci-dessus, si NEG est remplacé par DEC, le résultat sera également 0 ou 0xFFFFFFFF. Cependant, les valeurs sont inversées par rapport à l'utilisation de l'instruction NEG.

IV-D. Manipuler les bits en C

IV-D-1. Les opérateurs niveau bit du C

Contrairement à certains langages de haut niveau, le C fournit des opérateurs pour les opérations niveau bit. L'opération ET est représentée par l'opérateur binaire & (14). L'opération OU est représentée par l'opérateur binaire |. L'opération XOR est représentée par l'opérateur binaire ^ . Et l'opération NOT est représentée par l'opérateur unaire ~ .

Les opérations de décalage sont effectuées au moyen des opérateurs binaires et du C. L'opérateur effectue les décalages à gauche et l'opérateur effectue les décalages à droite. Ces opérateurs prennent deux opérandes. L'opérande de gauche est la valeur à décaler et l'opérande de droite est le nombre de bits à décaler. Si la valeur à décaler est d'un type non signé, un décalage logique est effectué. Si la valeur est d'un type signé (comme int), alors un décalage arithmétique est utilisé. Voici un exemple en C utilisant ces opérateurs :

Sélectionnez

1 short int s ; / on suppose que les short int font 16 bits /

2 short unsigned u;

3 s = &#8722;1; / s = 0xFFFF (complement a 2) /

4 u = 100; / u = 0x0064 /
5 u = u | 0x0100; / u = 0x0164 /
6 s = s & 0xFFF0; / s = 0xFFF0 /
7 s = s ^ u; / s = 0xFE94 /
8 u = u << 3; / u = 0x0B20 (décalage logique) /
9 s = s >> 2; / s = 0xFFA5 (décalage arithmetique) /

IV-D-2. Utiliser les opérateurs niveau bit en C

Les opérateurs niveau bit sont utilisés en C pour les mêmes raisons qu'ils le sont en assembleur. Ils permettent de manipuler les bits d'une donnée individuellement et peuvent être utilisés pour des multiplications et des divisions rapides. En fait, un compilateur C malin utilisera automatiquement un décalage pour une multiplication du type x *= 2.

Beaucoup d'API (15) de systèmes d'exploitation (comme POSIX(16) et Win32) contiennent des fonctions qui utilisent des opérandes donc les données sont codées sous la forme de bits. Par exemple, les systèmes POSIX conservent les permissions sur les fichiers pour trois différents types d'utilisateurs : utilisateur (un nom plus approprié serait propriétaire), groupe et autres. Chaque type d'utilisateur peut recevoir la permission de lire, écrire et/ou exécuter un fichier. Pour changer les permissions d'un fichier, le programmeur C doit manipuler des bits individuels. POSIX définit plusieurs macros pour l'aider (voir Tableau 3.6).

Image non disponible

La fonction chmod peut être utilisée pour définir les permissions sur un fichier. Cette fonction prend deux paramètres, une chaine avec le nom du fichier à modifier et un entier (17) avec les bits appropriés d'allumés pour les permissions désirées. Par exemple, le code ci-dessous définit les permissions pour permettre au propriétaire du fichier de lire et d'écrire dedans, au groupe de lire le fichier et d'interdire l'accès aux autres.

Sélectionnez

chmod("foo", S_IRUSR | S_IWUSR | S_IRGRP );

La fonction POSIX stat peut être utilisée pour récupérer les bits de permission en cours pour un fichier. En l'utilisant avec la fonction chmod,

il est possible de modifier certaines des permissions sans changer les autres.

Voici un exemple qui retire l'accès en écriture aux autres et ajoute les droits de lecture pour le propriétaire. Les autres permissions ne sont pas altérées.

Sélectionnez

1 struct stat le_stats ; / structure utilisee par stat () /

2 stat ("foo", & le_stats ); / lit les infos du fichier

3 le_stats .st_mode contient les bits de permission /

4 chmod("foo", ( le_stats .st_mode & ~S_IWOTH) | S_IRUSR);

IV-E. Représentations big et little endian

Le Chapitre II a introduit les concepts de représentations big et little endian des données multioctets. Cependant, l'auteur s'est rendu compte que pour beaucoup de gens, ce sujet est confus. Cette section couvre le sujet plus en détail.

Le lecteur se souvient sûrement que le caractère big ou little endian fait référence à l'ordre dans lequel les octets (pas les bits) d'un élément de données multioctets sont stockés en mémoire. La représentation big endian est la méthode la plus intuitive. Elle stocke l'octet le plus significatif en premier, puis le second octet le plus significatif, etc. En d'autres termes, les gros (big) bits sont stockés en premier. La méthode little endian stocke les octets dans l'ordre inverse (moins significatif en premier). La famille des processeurs x86 utilise la représentation little endian.

Par exemple, considérons le double mot représentant 1234567816. En représentation big endian, les octets seraient stockés 12 34 56 78. En représentation little endian, les octets seraient stockés 78 56 34 12.

Le lecteur est probablement en train de se demander pourquoi n'importe quel concepteur de puce sain d'esprit utiliserait la représentation little endian ?

Les ingénieurs de chez Intel étaient-ils sadiques pour infliger à une multitude de programmeurs cette représentation qui prête à confusion ? Il peut sembler que le processeur ait à faire du travail supplémentaire pour stocker les octets en mémoire dans l'ordre inverse (et pour les réinverser lorsqu'il lit à partir de la mémoire). En fait, le processeur ne fait aucun travail supplémentaire pour lire et écrire en mémoire en utilisant le format little endian. Il faut comprendre que le processeur est constitué de beaucoup de circuits électroniques qui ne travaillent que sur des valeurs de un bit. Les bits (et les octets) peuvent être dans n'importe quel ordre dans le processeur.

Considérons le registre de deux octets AX. Il peut être décomposé en deux registres d'un octet : AH et AL. Il y a des circuits dans le processeur qui conservent les valeurs de AH and AL. Ces circuits n'ont pas d'ordre dans le processeur. C'est-à-dire que les circuits pour AH ne sont pas avant ou après les circuits pour AL. Une instruction MOV qui copie la valeur de AX en mémoire copie la valeur de AL puis de AH. Ce n'est pas plus dur pour le processeur que de stocker AH en premier.

Le même argument s'applique aux bits individuels d'un octet. Ils ne sont pas réellement dans un ordre déterminé dans les circuits du processeur (ou en mémoire en ce qui nous concerne). Cependant, comme les bits individuels ne peuvent pas être adressés dans le processeur ou en mémoire, il n'y a pas de façon de savoir (et aucune raison de s'en soucier) l'ordre dans lequel ils sont conservés à l'intérieur du processeur.

Le code C de la Figure 3.4 montre comment le caractère big ou little endian d'un processeur peut être déterminé.

Image non disponible

Le pointeur p traite la variable word comme un tableau de caractères de deux éléments. Donc, p[0] correspond au premier octet de word en mémoire dont la valeur dépend du caractère big ou little endian du processeur.

IV-E-1. Quand se soucier du caractère big ou little endian

Pour la programmation courante, le caractère big ou little endian du processeur n'est pas important. Le moment le plus courant où cela devient important est lorsque des données binaires sont transférées entre différents systèmes informatiques. Cela se fait habituellement en utilisant un type quelconque de média physique (comme un disque) ou via un réseau. Comme les données ASCII sont sur un seul octet, le caractère big ou little endian n'est pas un problème.

Avec l'avènement des jeux de caractères multioctets comme UNICODE, le caractère big ou little endian devient important même pour les données texte. UNICODE supporte les deux types de représentation et a un mécanisme pour indiquer celle qui est utilisée pour représenter les données.

Tous les en-têtes internes de TCP/IP stockent les entiers au format big

endian (appelé ordre des octets réseau). Les bibliothèques TCP/IP offrent des fonctions C pour résoudre les problèmes de représentation big ou little endian d'une façon portable. Par exemple, la fonction htonl () convertit un double-mot (ou un entier long) depuis le format hôte vers le format réseau.

La fonction function ntohl () effectue la transformation inverse (18). Pour un système big endian les deux fonctions retournent leur paramètre inchangé. Cela permet d'écrire des programmes réseau qui compileront et s'exécuteront correctement sur n'importe quel système sans tenir compte de la représentation utilisée. Pour plus d'information sur les représentations big et little endian et la programmation réseau, voyez l'excellent livre de W. Richard Steven : UNIX Network Programming.

La Figure 3.5 montre une fonction C qui passe d'une représentation à l'autre pour un double mot.

Image non disponible

Le processeur 486 a introduit une nouvelle instruction machine appelée BSWAP qui inverse les octets de n'importe quel registre 32 bits. Par exemple,

Sélectionnez

bswap edx ; échange les octets de edx

L'instruction ne peut pas être utilisée sur des registres de 16 bits. Cependant, l'instruction XCHG peut être utilisée pour échanger les octets des registres 16 bits pouvant être décomposés en registres de 8 bits. Par exemple :

Sélectionnez

xchg ah,al ; échange les octets de ax

IV-F. Compter les bits

Plus haut, nous avons donné une technique intuitive pour compter le nombre de bits allumés dans un double-mot. Cette section décrit d'autres méthodes moins directes de le faire pour illustrer les opérations sur les bits dont nous avons parlé dans ce chapitre.

IV-F-1. Méthode une

La première méthode est très simple, mais pas évidente. La Figure 3.6 en montre le code.

Image non disponible

Comment fonctionne cette méthode ? À chaque itération de la boucle, un bit est éteint dans data. Quand tous les bits sont éteints ((i.e. lorsque data vaut zéro), la boucle s'arrête. Le nombre d'itérations requises pour mettre data à zéro est égal au nombre de bits dans la valeur originale de data.

La ligne 6 est l'endroit où un bit de data est éteint. Comment cela marche ? Considérons la forme générale de la représentation binaire de data et le 1 le plus à droite dans cette représentation. Par dénifition, chaque bit après ce 1 est à zéro. Maintenant, que sera la représentation de data - 1 ? Les bits à gauche du 1 le plus à droite seront les mêmes que pour data, mais à partir du 1 le plus à droite, les bits seront les compléments des bits originaux de data. Par exemple :

data = xxxxx10000

data - 1 = xxxxx01111

où les x sont les mêmes pour les deux nombres. Lorsque l'on applique un ET sur data avec data - 1, le résultat mettra le 1 le plus à droite de data à zéro et laissera les autres bits inchangés.

IV-F-2. Méthode deux

Un tableau de recherche peut également être utilisé pour contrer les bits de n'importe quel double-mot. L'approche intuitive serait de précalculer le nombre de bits de chaque double-mot et de le stocker dans un tableau.

Cependant, il y a deux problèmes relatifs à cette approche. Il y a à peu près 4 milliards de valeurs de doubles-mots ! Cela signifie que le tableau serait très gros et que l'initialiser prendrait beaucoup de temps (en fait, à moins que l'on ait l'intention d'utiliser le tableau plus de 4 milliards de fois, cela prendrait plus de temps de l'initialiser que cela n'en prendrait de calculer le nombre de bits avec la méthode un !).

Une méthode plus réaliste calculerait le nombre de bits pour toutes les valeurs possibles d'octet et les stockerait dans un tableau. Le double-mot peut alors être décomposé en quatre valeurs d'un octet. Le nombre de bits pour chacune de ces quatre valeurs d'un octet est recherché dans le tableau et additionné aux autres pour trouver le nombre de bits du double-mot original.

La Figure 3.7 montre le code implémentant cette approche.

Image non disponible

La fonction initialize_count_bits doit être appelée avant le premier appel à la fonction count_bits. Cette fonction initialise le tableau global byte_bit_count. La fonction count_bits ne considère pas la variable data comme un double-mot, mais comme un tableau de quatre octets. Donc, dword[0] est un des octets de data (soit le moins significatif, soit le plus significatif selon que le matériel est little ou big endian, respectivement).

Bien sûr, on peut utiliser une instruction comme :

(data >> 24) & 0x000000FF

pour trouver la valeur de l'octet le plus significatif et des opérations similaires pour les autres octets, cependant, ces opérations seraient plus lentes qu'une référence à un tableau.

Un dernier point, une boucle for pourrait facilement être utilisée pour calculer la somme des lignes 22 et 23. Mais, cela inclurait le supplément de l'initialisation de l'indice, sa comparaison après chaque itération et son incrémentation. Calculer la somme comme une somme explicite de quatre valeurs est plus rapide. En fait, un compilateur intelligent convertirait la version avec une boucle for en une somme explicite. Ce procédé de réduire ou éliminer les itérations d'une boucle est une technique d'optimisation de compilateur appelée loop unrolling (déroulage de boucle).

IV-F-3. Méthode trois

Il y a encore une méthode intelligente de compter les bits allumés d'une donnée. Cette méthode ajoute littéralement les 1 et les 0 de la donnée.

La somme est égale au nombre de 1 dans la donnée. Par exemple, considérons le comptage des 1 d'un octet stocké dans une variable nommée data. La première étape est d'effectuer l'opération suivante :

Sélectionnez

data = (data & 0x55) + ((data >> 1) & 0x55);

Pourquoi faire cela ? La constante hexa 0x55 vaut 01010101 en binaire. Dans le premier opérande de l'addition, on effectue un ET sur data avec elle, les bits sur des positions impaires sont supprimés. Le second opérande ((data>> 1) & 0x55) commence par déplacer tous les bits à des positions paires vers une position impaire et utilise le même masque pour supprimer ces bits.

Maintenant, le premier opérande contient les bits impairs et le second, les bits pairs de data. Lorsque ces deux opérandes sont additionnés, les bits pairs et les bits impairs de data sont additionnés. Par exemple, si data vaut 101100112, alors :

Image non disponible

L'addition à droite montre les bits additionnés ensemble. Les bits de l'octet sont divisés en deux champs de 2 bits pour montrer qu'il y a en fait quatre additions indépendantes. Comme la plus grande valeur que ces sommes peuvent prendre est deux, il n'est pas possible qu'une somme déborde de son champ et corrompe l'une des autres sommes.

Bien sûr, le nombre total de bits n'a pas encore été calculé. Cependant, la même technique que celle qui a été utilisée ci-dessus peut être utilisée pour calculer le total en une série d'étapes similaires. L'étape suivante serait :

data = (data & 0x33) + ((data >> 2) & 0x33);

En continuant l'exemple du dessus (souvenez-vous que data vaut maintenant 011000102).

Image non disponible

Il y a maintenant deux champs de 4 bits additionnés individuellement. La prochaine étape est d'additionner ces deux sommes de bits ensemble pour former le résultat final :

data = (data & 0x0F) + ((data >> 4) & 0x0F);

En utilisant l'exemple ci-dessus (avec data égale à 001100102) :

Image non disponible

Maintenant, data vaut 5 ce qui est le résultat correct. La Figure 3.8 montre une implémentation de cette méthode qui compte les bits dans un double-mot.

Image non disponible

Elle utilise une boucle for pour calculer la somme. Il serait plus rapide de dérouler la boucle, cependant, la boucle rend plus claire la façon dont la méthode se généralise à différentes tailles de données.


V. Sous-programmes

Ce chapitre explique comment utiliser des sous-programmes pour créer des programmes modulaires et s'interfacer avec des langages de haut niveau (comme le C). Les fonctions et les procédures sont des exemples de sous-programmes dans les langages de haut niveau.

Le code qui appelle le sous-programme et le sous-programme lui-même doivent se mettre d'accord sur la façon de se passer les données. Ces règles sur la façon de passer les données sont appelées conventions d'appel. Une grande partie de ce chapitre traitera des conventions d'appel standard du C qui peuvent être utilisées pour interfacer des sous-programmes assembleur avec des programmes C. Celles-ci (et d'autres conventions) passent souvent les adresses des données (i.e. des pointeurs) pour permettre au sous-programme d'accéder aux données en mémoire.

V-A. Adressage indirect

L'adressage indirect permet aux registres de se comporter comme des pointeurs. Pour indiquer qu'un registre est utilisé indirectement comme un pointeur, il est entouré par des crochets ([ ]). Par exemple :

1 mov ax, [Data] ; adressage mémoire direct normal d'un mot
2 mov ebx, Data ; ebx = & Data
3 mov ax, [ebx] ; ax = *ebx

Comme AX contient un mot, la ligne 3 lit un mot commençant à l'adresse stockée dans EBX. Si AX était remplacé par AL, un seul octet serait lu.

Il est important de réaliser que les registres n'ont pas de types comme les variables en C. Ce sur quoi EBX est censé pointer est totalement déterminé par les instructions utilisées. Si EBX est utilisé de manière incorrecte, il n'y aura souvent pas d'erreur signalée de la part de l'assembleur, cependant, le programme ne fonctionnera pas correctement. C'est une des nombreuses raisons pour lesquelles la programmation assembleur est plus sujette à erreur que la programmation de haut niveau.

Tous les registres 32 bits généraux (EAX, EBX, ECX, EDX) et d'index (ESI, EDI) peuvent être utilisés pour l'adressage indirect. En général, les registres 16 et 8 bits ne le peuvent pas.

V-B. Exemple de sous-programme simple

Un sous-programme est une unité de code indépendante qui peut être utilisée depuis différentes parties du programme. En d'autres termes, un sous-programme est comme une fonction en C. Un saut peut être utilisé pour appeler le sous-programme, mais le retour présente un problème. Si le sous-programme est destiné à être utilisé par différentes parties du programme, il doit revenir à la section de code qui l'a appelé. Donc, le retour du sous-programme ne peut pas être codé en dur par un saut vers une étiquette. Le code ci-dessous montre comment cela peut être réalisé en utilisant une forme indirecte de l'instruction JMP. Cette forme de l'instruction utilise la valeur d'un registre pour déterminer où sauter (donc, le registre agit plus comme un pointeur de fonction du C). Voici le premier programme du Chapitre II réécrit pour utiliser un sous-programme.

sub1.asm

1 ; fichier : sub1.asm
2 ; Programme d'exemple de sous-programme
3 %include "asm_io.inc"
4
5 segment .data
6 prompt1 db "Entrez un nombre : ", 0 ; ne pas oublier le zéro terminal
7 prompt2 db "Entrez un autre nombre : ", 0
8 outmsg1 db "Vous avez entré ", 0
9 outmsg2 db " et ", 0
10 outmsg3 db ", la somme des deux vaut ", 0
11
12 segment .bss
13 input1 resd 1
14 input2 resd 1
15
16 segment .text
17 global _asm_main
18 _asm_main :
19 enter 0,0 ; routine d'initialisation
20 pusha
21
22 mov eax, prompt1 ; affiche l'invite
23 call print_string
24
25 mov ebx, input1 ; stocke l'adresse de input1 dans ebx
26 mov ecx, ret1 ; stocke l'adresse de retour dans ecx
27 jmp short get_int ; lit un entier
28 ret1:
29 mov eax, prompt2 ; affiche l'invite
30 call print_string
31
32 mov ebx, input2
33 mov ecx, $ + 7 ; ecx = cette addresse + 7
34 jmp short get_int
35
36 mov eax, [input1] ; eax = dword dans input1
37 add eax, [input2] ; eax += dword dans input2
38 mov ebx, eax ; ebx = eax
39
40 mov eax, outmsg1
41 call print_string ; affiche le premier message
42 mov eax, [input1]
43 call print_int ; affiche input1
44 mov eax, outmsg2
45 call print_string ; affiche le second message
46 mov eax, [input2]
47 call print_int ; affiche input2
48 mov eax, outmsg3
49 call print_string ; affiche le troisième message
50 mov eax, ebx
51 call print_int ; affiche la somme (ebx)
52 call print_nl ; retour à la ligne
53
54 popa
55 mov eax, 0 ; retour au C
56 leave
57 ret
58 ; subprogram get_int
59 ; Paramètres :
60 ; ebx - addresse du dword dans lequel stocker l'entier
61 ; ecx - addresse de l'instruction vers laquelle retourner
62 ; Notes :
63 ; la valeur de eax est perdue
64 get_int:
65 call read_int
66 mov [ebx], eax ; stocke la saisie en mémoire
67 jmp ecx sub1.;asrmetour à l'appelant

Le sous-programme get_int utilise une convention d'appel simple, basée sur les registres. Il s'attend à ce que le registre EBX contienne l'adresse du DWORD dans lequel stocker le nombre saisi et à ce que le registre ECX contiennent l'adresse de l'instruction vers laquelle retourner.

Dans les lignes 25 à 28, l'étiquette ret1 est utilisée pour calculer cette adresse de retour.

Dans les lignes 32 à 34, l'opérateur $ est utilisé pour calculer l'adresse de retour. L'opérateur $ retourne l'adresse de la ligne sur laquelle il apparaît. L'expression $ + 7 calcule l'adresse de l'instruction MOV de la ligne 36.

Ces deux calculs d'adresses de code de retour sont compliqués. La première méthode requiert la définition d'une étiquette pour tout appel de sous-programme.

La seconde méthode ne requiert pas d'étiquette, mais nécessite de réfléchir attentivement. Si un saut proche avait été utilisé à la place d'un saut court, le nombre à ajouter à $ n'aurait pas été 7 !

Heureusement, il y a une façon plus simple d'appeler des sous-programmes. Cette méthode utilise la pile.

V-C. La pile

Beaucoup de processeurs ont un support intégré pour une pile. Une pile est une liste Last-In First-Out (LIFO, dernier entré, premier sorti). La pile est une zone de la mémoire qui est organisée de cette façon. L'instruction PUSH ajoute des données à la pile et l'instruction POP retire une donnée. La donnée retirée est toujours la dernière donnée ajoutée (c'est pourquoi on appelle ce genre de liste dernier entré, premier sorti).

Le registre de segment SS spécifie le segment qui contient la pile (habituellement c'est le même que celui qui contient les données). Le registre ESP contient l'adresse de la donnée qui sera retirée de la pile. On dit que cette donnée est au sommet de la pile. Les données ne peuvent être ajoutées que par unités de doubles-mots. C'est-à-dire qu'il est impossible de placer un octet seul sur la pile.

L'instruction PUSH insère un double-mot (19) sur la pile en ôtant 4 de ESP puis en stockant le double-mot en [ESP]. L'instruction POP lit le double-mot en [ESP] puis ajoute 4 à ESP. Le code ci-dessous montre comment fonctionnent ces instructions en supposant que ESP vaut initialement 1000H

1 push dword 1 ; 1 est stocké en 0FFCh, ESP = 0FFCh
2 push dword 2 ; 2 est stocké en 0FF8h, ESP = 0FF8h
3 push dword 3 ; 3 est stocké en 0FF4h, ESP = 0FF4h
4 pop eax ; EAX = 3, ESP = 0FF8h
5 pop ebx ; EBX = 2, ESP = 0FFCh
6 pop ecx ; ECX = 1, ESP = 1000h

La pile peut être utilisée comme un endroit approprié pour stocker des données temporairement. Elle est également utilisée pour effectuer des appels de sous-programmes, passer des paramètres et des variables locales.

Le 80x86 fournit également une instruction, PUSHA, qui empile les valeurs des registres EAX, EBX, ECX, EDX, ESI, EDI et EBP (pas dans cet ordre).

L'instruction POPA peut être utilisée pour les dépiler tous.

V-D. Les Instructions CALL et RET

Le 80x86 fournit deux instructions qui utilisent la pile pour effectuer des appels de sous-programmes rapidement et facilement. L'instruction CALL effectue un saut inconditionnel vers un sous-programme et empile l'adresse de l'instruction suivante. L'instruction RET dépile une adresse et saute à cette adresse. Lors de l'utilisation de ces instructions, il est très important de gérer la pile correctement afin que le chiffre correct soit dépilé par l'instruction RET !

Le programme précédent peut être réécrit pour utiliser ces nouvelles instructions en changeant les lignes 25 à 34 en ce qui suit :

et en changeant le sous-programme get_int en :

mov ebx, input1
call get_int
mov ebx, input2
call get_int

get_int :
call read_int
mov [ebx], eax
ret

Il y a plusieurs avantages à utiliser CALL et RET :

  • c'est plus simple ! ;
  • cela permet d'imbriquer des appels de sous-programmes facilement.

Notez que get_int appelle read_int. Cet appel empile une autre adresse à la fin du code de read_int se trouve un RET qui dépile l'adresse de retour et saute vers le code de get_int. Puis, lorsque le RET de get_int est exécuté, il dépile l'adresse de retour qui revient vers asm_main. Cela fonctionne correctement, car il s'agit d'une pile LIFO.

Souvenez-vous, il est très important de dépiler toute donnée qui est empilée. Par exemple, considérons le code suivant :

1 get_int :
2 call read_int
3 mov [ebx], eax
4 push eax
5 ret ; dépile la valeur de EAX, pas l'adresse de retour !!

Ce code ne reviendra pas correctement !

V-E. Conventions d'appel

Lorsqu'un sous-programme est appelé, le code appelant et le sous-programme (l'appelé) doivent s'accorder sur la façon de se passer les données. Les langages de haut niveau ont des manières standard de passer les données appelées « conventions d'appel ». Pour interfacer du code de haut niveau avec le langage assembleur, le code assembleur doit utiliser les mêmes conventions que le langage de haut niveau. Les conventions d'appel peuvent différer d'un compilateur à l'autre ou peuvent varier selon la façon dont le code est compilé (p.e. selon que les optimisations sont activées ou pas). Une convention universelle est que le code est appelé par une instruction CALL et revient par un RET.

Tous les compilateurs C PC supportent une convention d'appel qui sera décrite dans le reste de ce chapitre par étape. Ces conventions permettent de créer des sous-programmes réentrants. Un sous-programme réentrant peut être appelé depuis n'importe quel endroit du programme en toute sécurité (même depuis le sous-programme lui-même).

V-E-1. Passer les paramètres via la pile

Les paramètres d'un sous-programme peuvent être passés par la pile. Ils sont empilés avant l'instruction CALL. Comme en C, si le paramètre doit être modifié par le sous-programme, l'adresse de la donnée doit être passée, pas sa valeur. Si la taille du paramètre est inférieure à un double-mot, il doit être converti en un double-mot avant d'être empilé.

Les paramètres sur la pile ne sont pas dépilés par le sous-programme, à place, ils sont accédés depuis la pile elle-même. Pourquoi ? Comme ils doivent être empilés avant l'instruction CALL, l'adresse de retour devrait être dépilée avant tout (puis réempilée).

Souvent, les paramètres sont utilisés à plusieurs endroits dans le sous-programme. Habituellement, ils ne peuvent pas être conservés dans un registre durant toute la durée du sous-programme et devront être stockés en mémoire. Les laisser sur la pile conserve une copie de la donnée en mémoire qui peut être accédée depuis n'importe quel endroit du sous-programme.

Considérons un sous-programme auquel on passe un paramètre unique via la pile. Lorsque le sous-programme est appelé, la pile ressemble à la Figure 4.1. On peut accéder au paramètre en utilisant l'adressage indirect ([ESP+4] (20)).

Image non disponible

Lors de l'utilisation de l'adressage indirect, le processeur 80x86 accède à différents segments selon les registres utilisés dans l'expression d'adressage indirect. ESP (et EBP) utilisent le segment de pile alors que EAX, EBX, ECX et EDX utilisent le segment de données.
Cependant, ce n'est habituellement pas important pour la plupart des programmes en mode protégé, car pour eux, les segments de pile et de données sont les mêmes.

Si la pile est également utilisée dans le sous-programme pour stocker des données, le nombre à ajouter à ESP changera. Par exemple, la Figure 4.2 montre à quoi ressemble la pile si un DWORD est empilé.

Image non disponible

Maintenant, le paramètre se trouve en ESP + 8, plus en ESP + 4. Donc, utiliser ESP lorsque l'on fait référence à des paramètres peut être une source d'erreurs. Pour résoudre ce problème, le 80386 fournit un autre registre à utiliser : EBP. La seule utilité de ce registre est de faire référence à des données sur la pile. La convention d'appel C stipule qu'un sous-programme doit d'abord empiler la valeur de EBP puis définir EBP pour qu'il soit égal à ESP. Cela permet à ESP de changer au fur et à mesure que des données sont empilées ou dépilées sans modifier EBP. À la fin du programme, la valeur originale de EBP doit être restaurée (c'est pourquoi elle est sauvegardée au début du sous-programme).

La Figure 4.3 montre la forme générale d'un sous-programme qui suit ces conventions.

Image non disponible

Les lignes 2 et 3 de la Figure 4.3 constituent le prologue générique d'un sous-programme. Les lignes 5 et 6 constituent l'épilogue. La Figure 4.4 montre à quoi ressemble la pile immédiatement après le prologue.

Image non disponible

Maintenant, le paramètre peut être accédé avec [EBP + 8] depuis n'importe quel endroit du programme sans se soucier de ce qui a été empilé entretemps par le sous-programme.

Une fois le sous-programme terminé, les paramètres qui ont été empilés doivent être retirés. La convention d'appel C spécifie que c'est au code appelant de le faire. D'autres conventions sont différentes. Par exemple, la convention d'appel Pascal spécifie que c'est au sous-programme de retirer les paramètres (Il y a une autre forme de l'instruction RET qui permet de le faire facilement). Quelques compilateurs C supportent cette convention également.

Le mot-clé pascal est utilisé dans le prototype et la définition de la fonction pour indiquer au compilateur d'utiliser cette convention. En fait, la convention stdcall, que les fonctions de l'API C MS Windows utilisent, fonctionne également de cette façon. Quel est son avantage ? Elle est un petit peu plus efficace que la convention C. Pourquoi toutes les fonctions C n'utilisent-elles pas cette convention alors ? En général, le C autorise une fonction à avoir un nombre variable d'arguments (p.e., les fonctions printf et scanf).

Pour ce type de fonction, l'opération consistant à retirer les paramètres de la pile varie d'un appel à l'autre. La convention C permet aux instructions nécessaires à la réalisation de cette opération de varier facilement d'un appel à l'autre. Les conventions Pascal et stdcall rendent cette opération très compliquée. Donc, la convention Pascal (comme le langage Pascal) n'autorise pas ce type de fonction. MS Windows peut utiliser cette convention puisqu'aucune de ses fonctions d'API ne prend un nombre variable d'arguments.

La Figure 4.5 montre comment un sous-programme utilisant la convention d'appel C serait appelé.

Image non disponible

La ligne 3 retire le paramètre de la pile en manipulant directement le pointeur de pile. Une instruction POP pourrait également être utilisée, mais cela nécessiterait le stockage d'un paramètre inutile dans un registre. En fait, dans ce cas particulier, beaucoup de compilateurs utilisent une instruction POP ECX pour retirer le paramètre. Le compilateur utilise un POP plutôt qu'un ADD, car le ADD nécessite plus d'octets pour stocker l'instruction. Cependant, le POP change également la valeur de ECX ! Voici un autre programme exemple avec deux sous-programmes qui utilisent les conventions d'appel C dont nous venons de parler. La ligne 54 (et les autres) montre que plusieurs segments de données et de texte peuvent être déclarés dans un même fichier source. Ils seront combinés en des segments de données et de texte uniques lors de l'édition de liens. Diviser les données et le code en segments séparés permet d'avoir les données d'un sous-programme définies à proximité de celui-ci.

sub3.asm

1 %include "asm_io.inc"
2
3 segment .data
4 sum dd 0
5
6 segment .bss
7 input resd 1
8
9 ;
10 ; algorithme en pseudo-code
11 ; i = 1;
12 ; sum = 0;
13 ; while( get_int(i, &input), input != 0 ) {
14 ; sum += input;
15 ; i++;
16 ; }
17 ; print_sum(num);
18 segment .text
19 global _asm_main
20 _asm_main :
21 enter 0,0 ; routine d'initialisation
22 pusha
23
24 mov edx, 1 ; edx est le 'i' du pseudo-code
25 while_loop :
26 push edx ; empile i
27 push dword input ; empile l'adresse de input
28 call get_int
29 add esp, 8 ; dépile i et &input
30
31 mov eax, [input]
32 cmp eax, 0
33 je end_while
34
35 add [sum], eax ; sum += input
36
37 inc edx
38 jmp short while_loop
39
40 end_while:
41 push dword [sum] ; empile la valeur de sum
42 call print_sum
43 pop ecx ; dépile [sum]
44
45 popa
46 leave
47 ret
48
49 ; sous-programme get_int
50 ; Paramètres (dans l'ordre de l'empilement)
51 ; nombre de saisies (en [ebp + 12])
52 ; adresse du mot où stocker la saisie (en [ebp + 8])
53 ; Notes :
54 ; les valeurs de eax et ebx sont détruites
55 segment .data
56 prompt db ") Entrez un nombre entier (0 pour quitter): ", 0
57
58 segment .text
59 get_int:
60 push ebp
61 mov ebp, esp
62
63 mov eax, [ebp + 12]
64 call print_int
65
66 mov eax, prompt
67 call print_string
68
69 call read_int
70 mov ebx, [ebp + 8]
71 mov [ebx], eax ; stocke la saisie en mémoire
72
73 pop ebp
74 ret ; retour à l'appelant
75
76 ; sous-programme print_sum
77 ; affiche la somme
78 ; Paramètre :
79 ; somme à afficher (en [ebp+8])
80 ; Note : détruit la valeur de eax
81 ;
82 segment .data
83 result db "La somme vaut ", 0
84
85 segment .text
86 print_sum:
87 push ebp
88 mov ebp, esp
89
90 mov eax, result
91 call print_string
92
93 mov eax, [ebp+8]
94 call print_int
95 call print_nl
96
97 pop ebp
98 ret

V-E-2. Variables locales sur la pile

La pile peut être utilisée comme un endroit pratique pour stocker des variables locales. C'est exactement ce que fait le C pour les variables normales (ou automatiques en C lingo). Utiliser la pile pour les variables est important si l'on veut que les sous-programmes soient réentrants. Un programme réentrant fonctionnera qu'il soit appelé de n'importe quel endroit, même à partir du sous-programme lui-même. En d'autres termes, les sous-programmes réentrants peuvent être appelés récursivement.

Utiliser la pile pour les variables économise également de la mémoire. Les données qui ne sont pas stockées sur la pile utilisent de la mémoire du début à la fin du programme (le C appelle ce type de variables global ou static). Les données stockées sur la pile n'utilisent de la mémoire que lorsque le sous-programme dans lequel elles sont définies est actif.

Les variables locales sont stockées immédiatement après la valeur de EBP sauvegardée dans la pile. Elles sont allouées en soustrayant le nombre d'octets requis de ESP dans le prologue du sous-programme. La Figure 4.6 montre le nouveau squelette du sous-programme.

Image non disponible

Le registre EBP est utilisé pour accéder à des variables locales. Considérons la fonction C de la Figure 4.7.

Image non disponible

La Figure 4.8 montre comment le sous-programme équivalent pourrait être écrit en assembleur.

Image non disponible

La Figure 4.9 montre à quoi ressemble la pile après le prologue du programme de la Figure 4.8.

Image non disponible

Cette section de la pile qui contient les paramètres, les informations de retour et les variables locales est appelée cadre de pile (stack frame). Chaque appel de fonction C crée un nouveau cadre de pile sur la pile.

Le prologue et l'épilogue d'un sous-programme peuvent être simplifiés en utilisant deux instructions spéciales qui sont conçues spécialement dans ce but. L'instruction ENTER effectue le prologue et l'instruction LEAVE l'épilogue.

L'instruction ENTER prend deux opérandes immédiats. Dans la convention d'appel C, le deuxième opérande est toujours 0. Le premier opérande est le nombre d'octets nécessaires pour les variables locales. L'instruction LEAVE n'a pas d'opérande. La Figure 4.10 montre comment ces instructions sont utilisées. Notez que le squelette de programme (Figure 1.7) utilise également ENTER et LEAVE.

Image non disponible

En dépit du fait que ENTER et LEAVE simplifient le prologue et l'épilogue, ils ne sont pas utilisés très souvent. Pourquoi ? Parce qu'ils sont plus lents que les instructions plus simples équivalentes ! C'est un des exemples où il ne faut pas supposer qu'une instruction est plus rapide qu'une séquence de plusieurs instructions.

V-F. Programme multimodule

Un programme multimodule est un programme composé de plus d'un fichier objet. Tous les programmes présentés jusqu'ici sont des programmes multimodules. Ils consistent en un fichier objet C pilote et le fichier objet assembleur (plus les fichiers objet de la bibliothèque C). Souvenez-vous que l'éditeur de liens combine les fichiers objet en un programme exécutable unique. L'éditeur de liens doit rapprocher toutes les références faites à chaque étiquette d'un module (i.e. un fichier objet) de sa définition dans un autre module. Afin que le module A puisse utiliser une étiquette définie dans le module B, la directive extern doit être utilisée. Après la directive extern vient une liste d'étiquettes délimitées par des virgules. La directive indique à l'assembleur de traiter ces étiquettes comme externes au module. C'est-à-dire qu'il s'agit d'étiquettes qui peuvent être utilisées dans ce module, mais sont définies dans un autre. Le fichier asm_io.inc définit les routines read_int, etc. comme externes.

En assembleur, les étiquettes ne peuvent pas être accédées de l'extérieur par défaut. Si une étiquette doit pouvoir être accédée depuis d'autres modules que celui dans lequel elle est dénie, elle doit être déclarée comme globale dans son module, par le biais de la directive global. La ligne 13 du listing du programme squelette de la Figure 1.7 montre que l'étiquette _asm_main est définie comme globale. Sans cette déclaration, l'éditeur de liens indiquerait une erreur. Pourquoi ? Parce que le code C ne pourrait pas faire référence à l'étiquette interne _asm_main.

Voici le code de l'exemple précédent réécrit afin d'utiliser deux modules. Les deux sous-programmes (get_int et print_sum) sont dans des fichiers source distincts de celui de la routine _asm_main.

main4.asm

-
1 %include "asm_io.inc"
2
3 segment .data
4 sum dd 0
5
6 segment .bss
7 input resd 1
8
9 segment .text
10 global _asm_main
11 extern get_int, print_sum
12 _asm_main :
13 enter 0,0 ; routine d'initialisation
14 pusha
15
16 mov edx, 1 ; edx est le 'i' du pseudo-code
17 while_loop :
18 push edx ; empile i
19 push dword input ; empile l'adresse de input
20 call get_int
21 add esp, 8 ; dépile i et &input
22
23 mov eax, [input]
24 cmp eax, 0
25 je end_while
26
27 add [sum], eax ; sum += input
28
29 inc edx
30 jmp short while_loop
31
32 end_while:
33 push dword [sum] ; empile la valeur de sum
34 call print_sum
35 pop ecx ; dépile [sum]
36
37 popa
38 leave
39 ret

sub4.asm
Sélectionnez
1 %include "asm_io.inc"
2
3 segment .data
4 prompt db ") Entrez un nombre entier (0 pour quitter): ", 0
5
6 segment .text
7 global get_int, print_sum
8 get_int :
9 enter 0,0
10
11 mov eax, [ebp + 12]
12 call print_int
13
14 mov eax, prompt
15 call print_string
16
17 call read_int
18 mov ebx, [ebp + 8]
19 mov [ebx], eax ; stocke la saisie en mémoire
20
21 leave
22 ret ; retour à l'appelant
23
24 segment .data
25 result db "La somme vaut ", 0
26
27 segment .text
28 print_sum :
29 enter 0,0
30
31 mov eax, result
32 call print_string
33
34 mov eax, [ebp+8]
35 call print_int
36 call print_nl
37
38 leave
39 ret

L'exemple ci-dessus n'a que des étiquettes de code globales ; cependant, les étiquettes de donnée globales fonctionnent exactement de la même façon.

V-G. Interfacer de l'assembleur avec du C

Aujourd'hui, très peu de programmes sont écrits complètement en assembleur.

Les compilateurs sont très performants dans la conversion de code de haut niveau en code machine efficace. Comme il est plus facile d'écrire du code dans un langage de haut niveau, ils sont plus populaires. De plus, le code de haut niveau est beaucoup plus portable que l'assembleur !

Lorsque de l'assembleur est utilisé, c'est souvent pour de petites parties du code. Cela peut être fait de deux façons : en appelant des sous-routines assembleur depuis le C ou en incluant de l'assembleur. Inclure de l'assembleur permet au programmeur de placer des instructions assembleur directement dans le code C. Cela peut être très pratique ; cependant, il y a des inconvénients à inclure l'assembleur. Le code assembleur doit être écrit dans le format que le compilateur utilise. Aucun compilateur pour le moment ne supporte le format NASM. Des compilateurs différents demandent des formats différents. Borland et Microsoft demandent le format MASM. DJGPP et gcc sous Linux demandent le format GAS (21). La technique d'appel d'une sous-routine assembleur est beaucoup plus standardisée sur les PC.

Les routines assembleur sont habituellement utilisées avec le C pour les raisons suivantes :

  • un accès direct aux fonctionnalités matérielles de l'ordinateur est nécessaire, car il est difficile ou impossible d'y accéder en C ;
  • la routine doit être la plus rapide possible et le programmeur peut optimiser le code à la main mieux que le compilateur.

La dernière raison n'est plus aussi valide qu'elle l'était. La technologie des compilateurs a été améliorée au fil des ans et les compilateurs génèrent souvent un code très performant (en particulier si les optimisations sont activées).

Les inconvénients des routines assembleur sont une portabilité et une lisibilité réduites.

La plus grande partie des conventions d'appel C a déjà été présentée.

Cependant, il y a quelques fonctionnalités supplémentaires qui doivent être décrites.

V-G-1. Sauvegarder les registres

Tout d'abord, Le C suppose qu'une sous-routine maintient les valeurs des registres suivants : EBX, ESI, EDI, EBP, CS, DS, SS, ES. Cela ne signifie pas que la sous-routine ne les change pas en interne. Cela signifie que si elle change leurs valeurs, elle doit les restaurer avant de revenir. Les valeurs de EBX, ESI et EDI doivent être inchangées, car le C utilise ces registres pour les variables de registre. Habituellement, la pile est utilisée pour sauvegarder les valeurs originales de ces registres.

Le mot-clé register peut être utilisé dans une déclaration de variable C pour suggérer au compilateur d'utiliser un registre pour cette variable plutôt qu'un emplacement mémoire. On appelle ces variables, variables de registre. Les compilateurs modernes le font automatiquement sans qu'il y ait besoin d'une suggestion.

V-G-2. Étiquettes de fonctions

La plupart des compilateurs C ajoutent un caractère underscore(_) au début des noms des fonctions et des variables global/static. Par exemple, à une fonction appelée f sera assignée l'étiquette _f. Donc, s'il s'agit d'une routine assembleur, elle doit être étiquetée _f, pas f. Le compilateur Linux gcc, n'ajoute aucun caractère. Dans un exécutable Linux ELF, on utiliserait simplement l'étiquette f pour la fonction f. Cependant, le gcc de DJGPP ajoute un underscore. Notez que dans le squelette de programme assembleur (Figure 1.7), l'étiquette de la routine principale est _asm_main.

V-G-3. Passer des paramètres

Dans les conventions d'appel C, les arguments d'une fonction sont empilés sur la pile dans l'ordre inverse de celui dans lequel ils apparaissent dans l'appel de la fonction.

Considérons l'expression C suivante : printf("x = %d\n",x); la Figure 4.11 montre comment elle serait compilée (dans le format NASM équivalent).

Image non disponible

La Figure 4.12 montre à quoi ressemble la pile après le prologue de la fonction printf.

Image non disponible

La fonction printf est une des fonctions de la bibliothèque C qui peut prendre n'importe quel nombre d'arguments. Les règles des conventions d'appel C ont été spécialement écrites pour autoriser ce type de fonctions. Comme l'adresse de la chaîne format est empilée en dernier, son emplacement sur la pile sera toujours EBP + 8 quel que soit le nombre de paramètres passés à la fonction. Le code de printf peut alors analyser la chaîne format pour déterminer combien de paramètres ont dû être passés et les récupérer sur la pile.

Bien sûr, s'il y a une erreur, printf("x = %d\n"), le code de printf affichera quand même la valeur double-mot en [EBP + 12]. Cependant, ce ne sera pas la valeur de x !

Il n'est pas nécessaire d'utiliser l'assembleur pour gérer un nombre aléatoire d'arguments en C. L'en-tête stdarg.h définit des macros qui peuvent être utilisées pour l'effectuer de façon portable. Voyez n'importe quel bon livre sur le C pour plus de détails.

V-G-4. Calculer les adresses des variables locales

Trouver l'adresse d'une étiquette définie dans les segments data ou bss est simple. Basiquement, l'éditeur de liens le fait. Cependant, calculer l'adresse d'une variable locale (ou d'un paramètre) sur la pile n'est pas aussi intuitif.

Néanmoins, c'est un besoin très courant lors de l'appel de sous-routines.

Considérons le cas du passage de l'adresse d'une variable (appelons-la x) à une fonction (appelons-la foo). Si x est situé en EBP − 8 sur la pile, on ne peut pas utiliser simplement :

        mov eax, ebp – 8

Pourquoi ? La valeur que MOV stocke dans EAX doit être calculée par l'assembleur (c'est-à-dire qu'elle doit donner une constante). Cependant, il y a une instruction qui effectue le calcul désiré. Elle est appelée LEA (pour Load Eective Address, Charger l'Adresse Effective). L'extrait suivant calculerait l'adresse de x et la stockerait dans EAX :

        lea eax, [ebp - 8]

Maintenant, EAX contient l'adresse de x et peut être placé sur la pile lors de l'appel de la fonction foo. Ne vous méprenez pas, au niveau de la syntaxe, c'est comme si cette instruction lisait la donnée en [EBP−8]; cependant, ce n'est pas vrai. L'instruction LEA ne lit jamais la mémoire ! Elle calcule simplement l'adresse qui sera lue par une autre instruction et stocke cette adresse dans son premier opérande registre. Comme elle ne lit pas la mémoire, aucune taille mémoire (p.e. dword) n'est nécessaire ni autorisée.

V-G-5. Retourner des valeurs

Les fonctions C non void retournent une valeur. Les conventions d'appel C spécifient comment cela doit être fait. Les valeurs de retour sont passées via les registres. Tous les types entiers (char, int, enum, etc.) sont retournés dans le registre EAX. S'ils sont plus petits que 32 bits, ils sont étendus à 32 bits lors du stockage dans EAX (la façon dont ils sont étendus dépend du fait qu'ils sont signés ou non). Les valeurs 64 bits sont retournées dans la paire de registres EDX:EAX. Les valeurs de pointeurs sont également stockées dans EAX. Les valeurs en virgule flottante sont stockées dans le registre STP du coprocesseur arithmétique (ce registre est décrit dans le

chapitre sur les nombres en virgule flottante).

V-G-6. Autres conventions d'appel

Les règles ci-dessus décrivent les conventions d'appel C supportées par tous les compilateurs C 80x86. Souvent, les compilateurs supportent également d'autres conventions d'appel. Lorsqu'il y a une interface avec le langage assembleur, il est très important de connaître les conventions utilisées par le compilateur lorsqu'il appelle votre fonction. Habituellement, par défaut, ce sont les conventions d'appel standard qui sont utilisées, cependant, ce n'est pas toujours le cas (22). Les compilateurs qui utilisent plusieurs conventions ont souvent des options de ligne de commande qui peuvent être utilisées pour changer la convention par défaut. Ils fournissent également des extensions à la syntaxe C pour assigner explicitement des conventions d'appel à des fonctions de manière individuelle. Cependant, ces extensions ne sont pas standardisées et peuvent varier d'un compilateur à l'autre.

Le compilateur GCC autorise différentes conventions d'appel. La convention utilisée par une fonction peut être déclarée explicitement en utilisant l'extension __attribute__ . Par exemple, pour déclarer une fonction void qui utilise la convention d'appel standard appelée f qui ne prend qu'un paramètre int, utilisez la syntaxe suivante pour son prototype :

        void f ( int ) __attribute__((cdecl));

GCC supporte également la convention d'appel standard call . La fonction ci-dessus pourrait être déclarée afin d'utiliser cette convention en remplaçant le cdecl par stdcall. La différence entre stdcall et cdecl est que stdcall impose à la sous-routine de retirer les paramètres de la pile (comme le fait la convention d'appel Pascal). Donc, la convention stdcall ne peut être utilisée que par des fonctions qui prennent un nombre fixe d'arguments (i.e. celles qui ne sont pas comme printf et scanf).

GCC supporte également un attribut supplémentaire appelé regparm qui indique au compilateur d'utiliser les registres pour passer jusqu'à trois arguments entiers à une fonction au lieu d'utiliser la pile. C'est un type d'optimisation courant que beaucoup de compilateurs supportent.

Borland et Microsoft utilisent une syntaxe commune pour déclarer les conventions d'appel. Ils ajoutent les mots-clés __cdecl et __stdcall au C. Ces mots-clés se comportent comme des modicateurs de fonction et apparaissent immédiatement avant le nom de la fonction dans un prototype.

Par exemple, la fonction f ci-dessus serait définie comme suit par Borland et Microsoft :

        void __cdecl f( int );

Il y a des avantages et des inconvénients à chacune des conventions d'appel.

Le principal avantage de cdecl est qu'elle est simple et très flexible.

Elle peut être utilisée pour n'importe quel type de fonction C et sur n'importe quel compilateur C. Utiliser d'autres conventions peut limiter la portabilité de la sous-routine. Son principal inconvénient est qu'elle peut être plus lente que certaines autres et utilise plus de mémoire (puisque chaque appel de fonction nécessite du code pour retirer les paramètres de la pile).

L'avantage de la convention stdcall est qu'elle utilise moins de mémoire que cdecl. Aucun nettoyage de pile n'est requis après l'instruction CALL. Son principal inconvénient est qu'elle ne peut pas être utilisée avec des fonctions qui ont un nombre variable d'arguments.

L'avantage d'utiliser une convention qui se sert des registres pour passer des paramètres entiers est la rapidité. Le principal inconvénient est que la convention est plus complexe. Certains paramètres peuvent se trouver dans des registres et d'autres sur la pile.

V-G-7. Exemples

Voici un exemple qui montre comment une routine assembleur peut être interfacée avec un programme C (notez que ce programme n'utilise pas le programme assembleur squelette (Figure 1.7) ni le module driver.c).

main5.c

1 #include <stdio.h>
2 / prototype de la routine assembleur /
3 void calc_sum( int, int ) __attribute__((cdecl));
4
5 int main( void )
6 {
7 int n, sum;
8
9 printf ("Somme des entiers jusquà ' : ");
10 scanf("%d", &n);
11 calc_sum(n, &sum);
12 printf ("La somme vaut %d\n", sum);
13 return 0;
14 }

sub5.asm
1 ; sous-routine _calc_sum
2 ; trouve la somme des entiers de 1 à n
3 ; Paramètres :
4 ; n - jusqu'où faire la somme (en [ebp + 8])
5 ; sump - pointeur vers un entier dans lequel stocker la somme (en [ebp + 12])
6 ; pseudo-code C :
7 ; void calc_sum( int n, int * sump )
8 ; {
9 ; int i, sum = 0;
10 ; for( i=1; i <= n; i++ )
11 ; sum += i;
12 ; *sump = sum;
13 ; }
14
15 segment .text
16 global _calc_sum
17 ;
18 ; variable locale :
19 ; sum en [ebp-4]
20 _calc_sum :
21 enter 4,0 ; Fait de la place pour sum sur la pile
22 push ebx ; IMPORTANT !
23
24 mov dword [ebp-4],0 ; sum = 0
25 dump_stack 1, 2, 4 ; affiche la pile de ebp-8 à ebp+16
26 mov ecx, 1 ; ecx est le i du pseudocode
27 for_loop:
28 cmp ecx, [ebp+8] ; cmp i et n
29 jnle end_for ; si non i <= n, quitter
30
31 add [ebp-4], ecx ; sum += i
32 inc ecx
33 jmp short for_loop
34
35 end_for:
36 mov ebx, [ebp+12] ; ebx = sump
37 mov eax, [ebp-4] ; eax = sum
38 mov [ebx], eax
39
40 pop ebx ; restaure ebx
41 leave
42 ret

Pourquoi la ligne 22 de sub5.asm est si importante ? Parce que les conventions d'appel C imposent que la valeur de EBX ne soit pas modifiée par l'appel de fonction. Si ce n'est pas respecté, il est très probable que le programme ne fonctionne pas correctement.

La ligne 25 montre la façon dont fonctionne la macro dump_stack. Souvenez-vous que le premier paramètre est juste une étiquette numérique et les deuxième et troisième paramètres déterminent respectivement combien de doubles-mots elle doit afficher en dessous et au-dessus de EBP. La Figure 4.13 montre une exécution possible du programme.

Image non disponible

Pour cette capture, on peut voir que l'adresse du dword où stocker la somme est BFFFFB80 (en EBP + 12); le nombre jusqu'auquel additionner est 0000000A (en EBP + 8), l'adresse de retour pour la routine est 08048501 (en EBP + 4), la valeur sauvegardée de EBP est BFFFFB88 (en EBP), la valeur de la variable locale est 0 en (EBP - 4) et pour finir, la valeur sauvegardée de EBX est 4010648C (en EBP - 8).

La fonction calc_sum pourrait être réécrite pour retourner la somme plutôt que d'utiliser un pointeur. Comme la somme est une valeur entière, elle doit être placée dans le registre EAX. La ligne 11 du fichier main5.c deviendrait :

        sum = calc_sum(n);

De plus, le prototype de calc_sum devrait être altéré. Voici le code assembleur modifié :

sub6.asm

1 ; sous-routine _calc_sum
2 ; trouve la somme des entiers de 1 à n
3 ; Paramètres :
4 ; n - jusqu'où faire la somme (en [ebp + 8])
5 ; Valeur de retour :
6 ; valeur de la somme
7 ; pseudo-code C :
8 ; int calc_sum( int n )
9 ; {
10 ; int i, sum = 0;
11 ; for( i=1; i <= n; i++ )
12 ; sum += i;
13 ; return sum;
14 ; }
15 segment .text
16 global _calc_sum
17 ;
18 ; variable locale :
19 ; sum en [ebp-4]
20 _calc_sum :
21 enter 4,0 ; fait de la place pour la somme sur la pile
22
23 mov dword [ebp-4],0 ; sum = 0
24 mov ecx, 1 ; ecx est le i du pseudocode
25 for_loop:
26 cmp ecx, [ebp+8] ; cmp i et n
27 jnle end_for ; si non i <= n, quitter
28
29 add [ebp-4], ecx ; sum += i
30 inc ecx
31 jmp short for_loop
32
33 end_for:
34 mov eax, [ebp-4] ; eax = sum
35
36 leave
37 ret

V-G-8. Appeler des fonctions C depuis l'assembleur

Un des avantages majeurs d'interfacer le C et l'assembleur est que cela permet au code assembleur d'accéder à la grande bibliothèque C et aux fonctions utilisateur. Par exemple, si l'on veut appeler la fonction scanf pour lire un entier depuis le clavier. La Figure 4.14 montre comment le faire.

Image non disponible

Une chose très importante à se rappeler est que scanf suit les conventions d'appel C standard à la lettre. Cela signifie qu'elle préserve les valeurs des registres EBX, ESI et EDI, cependant, les registres EAX, ECX et EDX peuvent être modifiés ! En fait, EAX sera modifié, car il contiendra la valeur de retour de l'appel à scanf. Pour d'autres exemples d'interface entre l'assembleur et le C, observez le code dans asm_io.asm qui a été utilisé pour créer asm_io.obj.

V-H. Sous-programmes réentrants et récursifs

Un sous-programme réentrant remplit les critères suivants :
  • il ne doit pas modifier son code. Dans un langage de haut niveau, cela serait difficile, mais en assembleur, il n'est pas si dur que cela pour un programme de modifier son propre code. Par exemple :
        mov word [cs :$+7], 5 ; copie 5 dans le mot 7 octets plus loin

        add ax, 2 ; l'expression précédente change 2 en 5 !
  • Il ne doit pas modifier de données globales (comme celles qui se trouvent dans les segments data et bss). Toutes les variables sont stockées sur la pile.

Il y a plusieurs avantages à écrire du code réentrant :

  • un sous-programme réentrant peut être appelé récursivement ;
  • un programme réentrant peut être partagé par plusieurs processus ;
  • sur beaucoup de systèmes d'exploitation multitâches, s'il y a plusieurs instances d'un programme en cours, seule une copie du code se trouve en mémoire. Les bibliothèques partagées et les DLL (Dynamic Link Libraries, Bibliothèques de Lien Dynamique) utilisent le même principe ;
  • les sous-programmes réentrants fonctionnent beaucoup mieux dans les programmes multithreadés (23). Windows 9x/NT et la plupart des systèmes d'exploitation de style Unix (Solaris, Linux, etc.) supportent les programmes multithreadés.

V-H-1. Sous-programmes récursifs

Ce type de sous-programmes s'appellent eux-mêmes. La récursivité peut être soit directe soit indirecte. La récursivité directe survient lorsqu'un sous-programme, disons foo, s'appelle lui-même dans le corps de foo. La récursivité indirecte survient lorsqu'un sous-programme ne s'appelle pas directement lui-même, mais via un autre sous-programme qu'il appelle. Par exemple, le sous-programme foo pourrait appeler bar et bar pourrait appeler foo.

Les sous-programmes récursifs doivent avoir une condition de terminaison.

Lorsque cette condition est vraie, il n'y a plus d'appel récursif. Si une routine récursive n'a pas de condition de terminaison ou que la condition n'est jamais remplie, la récursivité ne s'arrêtera jamais (exactement comme une boucle infinie).

La Figure 4.15 montre une fonction qui calcule une factorielle récursivement.

Elle peut être appelée depuis le C avec :

Sélectionnez

x = fact (3); / trouve 3! /

Image non disponible

La Figure 4.16 montre à quoi ressemble la pile au point le plus profond pour l'appel de fonction ci-dessus.

Image non disponible

Les Figures 4.17 et 4.18 montrent un exemple récursif plus compliqué en C et en assembleur, respectivement.

Image non disponible Image non disponible

Quelle est la sortie pour f(3) ? Notez que l'instruction ENTER crée un nouveau i sur la pile pour chaque appel récursif.

Donc, chaque instance récursive de f a sa propre variable i indépendante.

Définir i comme un double-mot dans le segment data ne fonctionnerait pas pareil.

V-H-2. Révision des types de stockage des variables en C

Le C fournit plusieurs types de stockage des variables :

global : ces variables sont déclarées en dehors de toute fonction et sont stockées à des emplacements mémoire fixes (dans les segments data ou bss) et existent depuis le début du programme jusqu'à la fin. Par défaut, on peut y accéder de n'importe quelle fonction dans le programme, cependant, si elles sont déclarées comme static, seules les fonctions dans le même module peuvent y accéder (i.e. en termes assembleur, l'étiquette est interne, pas externe) ;

static : il s'agit des variables locales d'une fonction qui sont déclarées static (malheureusement, le C utilise le mot-clé static avec deux sens différents !). Ces variables sont également stockées dans des emplacements mémoire fixes (dans data ou bss), mais ne peuvent être accédées directement que dans la fonction où elles sont définies ;

automatic : c'est le type par défaut d'une variable C dénie dans une fonction. Ces variables sont allouées sur la pile lorsque la fonction dans laquelle elles sont définies est appelée et sont désallouées lorsque la fonction revient. Donc, elles n'ont pas d'emplacement mémoire fixe.

register ; ce mot-clé demande au compilateur d'utiliser un registre pour la donnée dans cette variable. Il ne s'agit que d'une requête. Le compilateur n'a pas à l'honorer. Si l'adresse de la variable est utilisée à un endroit quelconque du programme, elle ne sera pas respectée (puisque les registres n'ont pas d'adresse). De plus, seuls les types entiers simples peuvent être des valeurs registres. Les types structures ne peuvent pas l'être : ils ne tiendraient pas dans un registre ! Les compilateurs C placent souvent les variables automatiques normales dans des registres sans aide du programmeur.

volatile : ce mot-clé indique au compilateur que la valeur de la variable peut changer à tout moment. Cela signifie que le compilateur ne peut faire aucune supposition sur le moment où la variable est modifiée. Souvent, un compilateur stocke la valeur d'une variable dans un registre temporairement et utilise le registre à la place de la variable dans une section de code. Il ne peut pas faire ce type d'optimisations avec les variables volatiles. Un exemple courant de variable volatile serait une variable qui peut être modifiée par deux threads d'un programme multithreadé. Considérons le code suivant :

1 x = 10;
2 y = 20;
3 z = x;

Si x pouvait être altéré par un autre thread, il est possible que l'autre thread change x entre les lignes 1 et 3, alors z ne vaudrait pas 10.

Cependant, si x n'a pas été déclaré comme volatile, le compilateur peut supposer que x est inchangé et positionner z à 10.

Une autre utilisation de volatile est d'empêcher le compilateur d'utiliser un registre pour une variable.


VI. Tableaux

VI-A. Introduction

Un tableau est un bloc contigu de données en mémoire. Tous les éléments de la liste doivent être du même type et occuper exactement le même nombre d'octets en mémoire. En raison de ces propriétés, les tableaux permettent un accès efficace à une donnée par sa position (ou indice) dans le tableau.

L'adresse de n'importe quel élément peut être calculée en connaissant les trois choses suivantes :

  • l'adresse du premier élément du tableau ;
  • le nombre d'octets de chaque élément ;
  • l'indice de l'élément.

Il est pratique de considérer l'indice du premier élément du tableau comme étant 0 (comme en C). Il est possible d'utiliser d'autres valeurs pour le premier indice, mais cela complique les calculs.

VI-A-1. Définir des tableaux

VI-A-1-a. Définir des tableaux dans les segments data et bss

Pour définir un tableau initialisé dans le segment data, utilisez les directives db, dw, etc. normales. directives. NASM fournit également une directive utile appelée TIMES qui peut être utilisée pour répéter une expression de nombreuses fois sans avoir à la dupliquer à la main. La Figure 5.1 montre plusieurs exemples.

Image non disponible

Pour définir un tableau non initialisé dans le segment bss, utilisez les directives resb, resw, etc. Souvenez-vous que ces directives ont un opérande qui spécifie le nombre d'unités mémoire à réserver. La Figure 5.1 montre également des exemples de ces types de définitions.

VI-A-1-b. Définir des tableaux comme variables locales

Il n'y a pas de manière directe de définir un tableau comme variable locale sur la pile. Comme précédemment, on calcule le nombre total d'octets requis pour toutes les variables locales, y compris les tableaux, et on l'ôte de ESP (soit directement, soit en utilisant l'instruction ENTER). Par exemple, si une fonction a besoin d'une variable caractère, deux entiers double-mot et un tableau de 50 éléments d'un mot, il faudrait 1+2×4+50×2 = 109 octets.

Cependant, le nombre ôté de ESP doit être un multiple de quatre (112 dans ce cas) pour maintenir ESP sur un multiple de double-mot. On peut organiser les variables dans ces 109 octets de plusieurs façons. La Figure 5.2 montre deux manières possibles.

Image non disponible

La partie inutilisée sert à maintenir les doubles-mots sur des adresses multiples de doubles-mots afin d'accélérer les accès mémoire.

VI-A-2. Accéder aux éléments de tableaux

Il n'y a pas d'opérateur [ ] en langage assembleur comme en C. Pour accéder à un élément d'un tableau, son adresse doit être calculée. Considérons les deux définitions de tableau suivantes :

Sélectionnez

array1 db 5, 4, 3, 2, 1 ; tableau d'octets

array2 dw 5, 4, 3, 2, 1 ; tableau de mots

Voici quelques exemples utilisant ces tableaux :

Sélectionnez

1 mov al, [array1] ; al = array1[0]

2 mov al, [array1 + 1] ; al = array1[1]

3 mov [array1 + 3], al ; array1[3] = al

4 mov ax, [array2] ; ax = array2[0]

5 mov ax, [array2 + 2] ; ax = array2[1] (PAS array2[2]!)

6 mov [array2 + 6], ax ; array2[3] = ax

7 mov ax, [array2 + 1] ; ax = ??

À la ligne ProductID="5, l" w:st="on">5, l'élément 1 du tableau de mots est référencé, pas l'élément 2.

Pourquoi ? Les mots sont des unités de deux octets, donc pour se déplacer à l'élément suivant d'un tableau de mots, on doit se déplacer de deux octets, pas d'un seul.

La ligne 7 lit un octet du premier élément et un octet du suivant. En C, le compilateur regarde le type de pointeur pour déterminer de combien d'octets il doit se déplacer dans une expression utilisant l'arithmétique des pointeurs, afin que le programmeur n'ait pas à le faire.

Cependant, en assembleur, c'est au programmeur de prendre en compte la taille des éléments du tableau lorsqu'il se déplace parmi les éléments.

La Figure 5.3 montre un extrait de code qui additionne tous les éléments de array1 de l'exemple de code précédent.

Image non disponible

À la ligne 7, AX est ajouté à DX. Pourquoi pas AL ? Premièrement, les deux opérandes de l'instruction ADD doivent avoir la même taille. Deuxièmement, il serait facile d'ajouter des octets et d'obtenir une somme qui serait trop grande pour tenir sur un octet. En utilisant DX, la somme peut atteindre 65 535. Cependant, il est important de réaliser que AH est également additionné. C'est pourquoi, AH est positionné à zéro (24) à la ligne 3.

Les Figures 5.4 et 5.5 montrent deux manières alternatives de calculer la somme. Les lignes en italiques remplacent les lignes 6 et 7 de la Figure 5.3.

Image non disponible Image non disponible

VI-A-3. Adressage indirect plus avancé

Ce n'est pas étonnant, l'adressage indirect est souvent utilisé avec les tableaux. La forme la plus générale d'une référence mémoire indirecte est :

Sélectionnez

[ reg de base + facteur *reg d'index + constante ]

où :

reg de base est un des registres EAX, EBX, ECX, EDX, EBP, ESP, ESI ou EDI ;

facteur est 1, 2, 4 ou 8 (s'il vaut 1, le facteur est omis) ;

reg d'index est un des registres EAX, EBX, ECX, EDX, EBP, ESI, EDI (notez que ESP n'est pas dans la liste) ;

constante est une constante 32 bits. Cela peut être une étiquette (ou une expression d'étiquette).

VI-A-4. Exemple

Voici un exemple qui utilise un tableau et le passe à une fonction. Il utilise le programme array1c.c (dont le listing suit) comme pilote, pas le programme driver.c.

array1.asm

Sélectionnez

1 ?fine ARRAY_SIZE 100
2 ?fine NEW_LINE 10
3
4 segment .data
5 FirstMsg db "10 premiers éléments du tableau", 0
6 Prompt db "Entrez l'indice de l'élément à afficher : ", 0
7 SecondMsg db "L'élément %d vaut %d", NEW_LINE, 0
8 ThirdMsg db "Éléments 20 à 29 du tableau", 0
9 InputFormat db "%d", 0
10
11 segment .bss
12 array resd ARRAY_SIZE
13
14 segment .text
15 extern _puts, _printf, _scanf, _dump_line
16 global _asm_main
17 _asm_main :
18 enter 4,0 ; variable locale dword en EBP – 4
19 push ebx
20 push esi
21
22 ; initialise le tableau à 100, 99, 98, 97...
23
24 mov ecx, ARRAY_SIZE
25 mov ebx, array
26 init_loop:
27 mov [ebx], ecx
28 add ebx, 4
29 loop init_loop
30
31 push dword FirstMsg ; affiche FirstMsg
32 call _puts
33 pop ecx
34
35 push dword 10
36 push dword array
37 call _print_array ; affiche les 10 premiers éléments du tableau
38 add esp, 8
39
40 ; demande à l'utilisateur l'indice de l'élément
41 Prompt_loop:
42 push dword Prompt
43 call _printf
44 pop ecx
45
46 lea eax, [ebp-4] ; eax = adresse du dword local
47 push eax
48 push dword InputFormat
49 call _scanf
50 add esp, 8
51 cmp eax, 1 ; eax = valeur de retour de scanf
52 je InputOK
53
54 call _dump_line ; ignore le reste de la ligne et recommence
55 jmp Prompt_loop ; si la saisie est invalide
56
57 InputOK:
58 mov esi, [ebp-4]
59 push dword [array + 4*esi]
60 push esi
61 push dword SecondMsg ; affiche la valeur de l'élément
62 call _printf
63 add esp, 12
64
65 push dword ThirdMsg ; affiche les éléments 20 à 29
66 call _puts
67 pop ecx
68
69 push dword 10
70 push dword array + 20*4 ; adresse de array[20]
71 call _print_array
72 add esp, 8
73
74 pop esi
75 pop ebx
76 mov eax, 0 ; retour au C
77 leave
78 ret
79
80 ;
81 ; routine _print_array
82 ; Routine appelable depuis le C qui affiche les éléments d'un tableau de doubles mots
83 ; comme des entiers signés.
84 ; Prototype C :
85 ; void print_array( const int * a, int n);
86 ; Paramètres :
87 ; a - pointeur vers le tableau à afficher (en ebp+8 sur la pile)
88 ; n - nombre d'entiers à afficher (en ebp+12 sur la pile)
89
90 segment .data
91 OutputFormat db "%-5d ]", NEW_LINE, 0
92
93 segment .text
94 global _print_array
95 _print_array :
96 enter 0,0
97 push esi
98 push ebx
99
100 xor esi, esi ; esi = 0
101 mov ecx, [ebp+12] ; ecx = n
102 mov ebx, [ebp+8] ; ebx = adresse du tableau
103 print_loop :
104 push ecx ; printf change ecx !
105
106 push dword [ebx + 4*esi] ; empile tableau[esi]
107 push esi
108 push dword OutputFormat
109 call _printf
110 add esp, 12 ; retire les paramètres (laisse ecx !)
111
112 inc esi
113 pop ecx
114 loop print_loop
115
116 pop ebx
117 pop esi
118 leave
119 ret

array1c.c

Sélectionnez

1 #include <stdio.h>
2
3  int asm_main( void );
4 void dump_line( void );
5
6 int main()
7 {
8 int ret_status ;
9 ret_status = asm_main();
10 return ret_status ;
11 }
12
13 /
14 fonction dump_line
15 retire tous les caractères restant sur la ligne courante dans le buffer d'entreé
16 /
17 void dump_line()
18 {
19 int ch;
20
21 while( (ch = getchar()) != EOF && ch != '\n')
22 / null body/ ;
23 }

VI-A-4-a. L'instruction LEA revisitée

L'instruction LEA peut être utilisée dans d'autres cas que le calcul d'adresse.

Elle est assez couramment utilisée pour les calculs rapides. Considérons le code suivant :

Sélectionnez

lea ebx, [4*eax + eax]

Il stocke la valeur de 5×EAX dans EBX. Utiliser LEA dans ce cas est à la fois plus simple et plus rapide que d'utiliser MUL. Cependant, il faut être conscient du fait que l'expression entre crochets doit être une adresse indirecte légale.

Donc, par exemple, cette instruction ne peut pas être utilisée pour multiplier par 6 rapidement.

VI-A-5. Tableaux Multidimensionnels

Les tableaux multidimensionnels ne sont pas vraiment différents de ceux à une dimension dont nous avons déjà parlé. En fait, ils sont représentés en mémoire exactement comme cela, un tableau à une dimension.

VI-A-5-a. Tableaux à Deux Dimensions

Ce n'est pas étonnant, le tableau multidimensionnel le plus simple est celui à deux dimensions. Un tableau à deux dimensions est souvent représenté comme une grille d'éléments. Chaque élément est identifié par une paire d'indices. Par convention, le premier indice est associé à la ligne et le second à la colonne.

Considérons un tableau avec trois lignes et deux colonnes défini de la manière suivante :

Sélectionnez

int a [3][2];

Le compilateur C réserverait de la place pour un tableau d'entiers de 6 (= 2 × 3) et placerait les éléments comme suit :

Image non disponible

Ce que ce tableau tente de montrer et que l'élément référencé comme a[0][0] est stocké au début du tableau de 6 éléments à une dimension. L'élément a[0][1] est stocké à la position suivante (indice 1), etc. Chaque ligne du tableau à deux dimensions est stockée en mémoire de façon contigüe. Le dernier élément d'une ligne est suivi par le premier élément de la suivante. On appelle cela la représentation au niveau ligne (rowwise) du tableau et c'est comme cela qu'un compilateur C/C++ représenterait le tableau.

Comment le compilateur détermine où a[i][j] se trouve dans la représentation au niveau ligne ? Une formule simple calcule l'indice à partir de i et j. La formule dans ce cas est 2i + j. Il n'est pas compliqué de voir comment on obtient cette formule. Chaque ligne fait deux éléments de long, donc le premier élément de la ligne i est à l'emplacement 2i. Puis on obtient l'emplacement de la colonne j en ajoutant j à 2i. Cette analyse montre également comment la formule est généralisée à un tableau de N colonnes : N × i + j.

Notez que la formule ne dépend pas du nombre de lignes.

Pour illustrer, voyons comment gcc compile le code suivant (utilisant le tableau a défini plus haut) :

Sélectionnez

x = a[ i ][ j ];

La Figure 5.6 montre le code assembleur correspondant.

Image non disponible

Donc, le compilateur convertit grossièrement le code en :

Sélectionnez

x = (&a[0][0] + 2i + j );

et en fait, le programmeur pourrait l'écrire de cette manière et obtenir le même résultat.

Il n'y a rien de magique à propos du choix de la représentation niveau ligne du tableau. Une représentation niveau colonne fonctionnerait également :

Image non disponible

Dans la représentation niveau colonne, chaque colonne est stockée de manière contigüe. L'élément [i][j] est stocké à l'emplacement i+3j. D'autres langages (FORTRAN, par exemple) utilisent la représentation niveau colonne.

C'est important lorsque l'on interface du code provenant de multiples langages.

VI-A-5-b. Dimensions supérieures à deux

Pour les dimensions supérieures à deux, la même idée de base est appliquée. Considérons un tableau à trois dimensions :

Sélectionnez

int b [4][3][2];

Ce tableau serait stocké comme s'il était composé de trois tableaux à deux dimensions, chacun de taille [3][2] stockés consécutivement en mémoire. Le tableau ci-dessous montre comment il commence :

Image non disponible

La formule pour calculer la position de b[i][j][k] est 6i + 2j + k. Le 6 est déterminé par la taille des tableaux [3][2]. En général, pour un tableau de dimension a[L][M][N] l'emplacement de l'élément a[i][j][k] sera M ×N ×i+N ×j +k. Notez, là encore, que la dimension L n'apparaît pas dans la formule.

Pour les dimensions plus grandes, le même procédé est généralisé. Pour un tableau à n dimensions de dimension D1 à Dn, l'emplacement d'un élément repéré par les indices i1 à in est donné par la formule :

D2 × D3 · · · × Dn × i1 + D3 × D4 · · · × Dn × i2 + · · · + Dn × in−1 + in

ou pour les fanas de maths, on peut l'écrire de façon plus concise :

Image non disponible

La première dimension, D1, n'apparaît pas dans la formule.

Pour la représentation au niveau colonne, la formule générale serait :

i1 +D1 ×i2 +· · ·+D1 ×D2 ×· · ·×Dn−2 ×in−1 +D1 ×D2 ×· · ·×Dn−1 ×in

ou dans la notation des fanas de maths :

Image non disponible

Dans ce cas, c'est la dernière dimension, Dn, qui n'apparaît pas dans la formule.

C'est là que vous comprenez que l'auteur est un major de physique (ou la référence à FORTRAN vous avait déjà mis sur la voie ?)

VI-A-5-c. Passer des tableaux multidimensionnels comme paramètres en C

La représentation au niveau ligne des tableaux multidimensionnels a un effet direct sur la programmation C. Pour les tableaux à une dimension, la taille du tableau n'est pas nécessaire pour calculer l'emplacement en mémoire de n'importe quel élément. Ce n'est pas vrai pour les tableaux multidimensionnels.

Pour accéder aux éléments de ces tableaux, le compilateur doit connaître toutes les dimensions sauf la première. Cela saute aux yeux lorsque l'on observe le prototype d'une fonction qui prend un tableau multidimensionnel comme paramètre. Ce qui suit ne compilera pas :

Sélectionnez

void f ( int a [ ][ ] ); / pas d' information sur la dimension /

Cependant, ce qui suit compile :

Sélectionnez

void f ( int a [ ][2] );

Tout tableau à deux dimensions à deux colonnes peut être passé à cette fonction. La première dimension n'est pas nécessaire (25).

Ne confondez pas avec une fonction ayant ce prototype :

Sélectionnez

void f ( int a [ ] );

Cela définit un tableau à une dimension de pointeurs sur des entiers (qui peut à son tour être utilisé pour créer un tableau de tableaux qui se comportent plus comme un tableau à deux dimensions).

Pour les tableaux de dimensions supérieures, toutes les dimensions sauf la première doivent être données pour les paramètres. Par exemple, un paramètre tableau à quatre dimensions peut être passé de la façon suivante :

Sélectionnez

void f ( int a [ ][4][3][2] );

VI-B. Instructions de tableaux/chaînes

La famille des processeurs 80x86 fournit plusieurs instructions conçues pour travailler avec les tableaux. Ces instructions sont appelées instructions de chaînes. Elles utilisent les registres d'index (ESI et EDI) pour effectuer une opération puis incrémentent ou décrémentent automatiquement l'un des registres d'index ou les deux. Le drapeau de direction (DF) dans le registre FLAGS détermine si les registres d'index sont incrémentés ou décrémentés.

Il y a deux instructions qui modifient le drapeau de direction :

CLD éteint le drapeau de direction. Les registres d'index sont alors incrémentés ;

STD allume le drapeau de direction. Les registres d'index sont alors décrémentés.

Une erreur très courante dans la programmation 80x86 est d'oublier de positionner le drapeau de direction. Cela conduit souvent à un code qui fonctionne la plupart du temps (lorsque le drapeau de direction est dans la position désirée), mais ne fonctionne pas tout le temps.

VI-B-1. Lire et écrire en mémoire

Les instructions de chaînes les plus simples lisent ou écrivent en mémoire, ou les deux. Elles peuvent lire ou écrire un octet, un mot ou un double-mot à la fois. La Figure 5.7 montre ces instructions avec une brève description de ce qu'elles font en pseudo-code.

Image non disponible

Il y a plusieurs choses à noter ici. Tout d'abord, ESI est utilisé pour lire et EDI pour écrire. C'est facile à retenir si l'on se souvient que SI signifie Source Index (Indice Source) et DI Destination Index (Indice de Destination). Ensuite, notez que le registre qui contient la donnée est fixe (AL, AX ou EAX). Enfin, notez que les instructions de stockage utilisent ES pour déterminer le segment dans lequel écrire, pas DS. En programmation en mode protégé, ce n'est habituellement pas un problème, puisqu'il n'y a qu'un segment de données et ES est automatiquement initialisé pour y faire référence (tout comme DS). Cependant, en programmation en mode réel, il est très important pour le programmeur d'initialiser ES avec la valeur de sélecteur de segment correcte (26). La Figure 5.8 montre un exemple de l'utilisation de ces instructions qui copie un tableau dans un autre.

Image non disponible

La combinaison des instructions LODSx et STOSx (comme aux lignes 13 et 14 de la Figure 5.8) est très courante. En fait, cette combinaison peut être effectuée par une seule instruction de chaînes MOVSx. La Figure 5.9 décrit les opérations effectuées par cette instruction.

Image non disponible

Les lignes 13 et 14 de la Figure 5.8 pourraient être remplacées par une seule instruction MOVSD avec le même résultat. La seule différence serait que le registre EAX ne serait pas utilisé du tout dans la boucle.

VI-B-2. Le préfixe d'instruction REP

La famille 80x86 fournit un préfixe d'instruction spécial (27) appelé REP qui peut être utilisé avec les instructions de chaînes présentées ci-dessus. Ce préfixe indique au processeur de répéter l'instruction de chaînes qui suit un nombre précis de fois. Le registre ECX est utilisé pour compter les itérations (exactement comme pour l'instruction LOOP). En utilisant le préfixe REP, la boucle de la Figure 5.8 (lignes 12 à 15) pourrait être remplacée par une seule ligne :

Sélectionnez

rep movsd

La Figure 5.10 montre un autre exemple qui met à zéro le contenu d'un tableau.

Image non disponible

VI-B-3. Instructions de comparaison de chaînes

La Figure 5.11 montre plusieurs nouvelles instructions de chaînes qui peuvent être utilisées pour comparer des données en mémoire entre elles ou avec des registres. Elles sont utiles pour comparer des tableaux ou effectuer des recherches. Elles positionnent le registre FLAGS exactement comme l'instruction CMP. Les instructions CMPSx comparent les emplacements mémoire correspondants et les instructions SCASx scannent des emplacements mémoire pour une valeur particulière.

La Figure 5.12 montre un court extrait de code qui recherche le nombre 12 dans un tableau de doubles-mots.

Image non disponible

L'instruction SCASD à la ligne 10 ajoute toujours 4 à EDI, même si la valeur recherchée est trouvée. Donc, si l'on veut trouver l'adresse du 12 dans le tableau, il est nécessaire de soustraire 4 de EDI (comme le fait la ligne 16).

VI-B-4. Les préfixes d'instruction REPx

Il y a plusieurs autres préfixes d'instruction du même genre que REP qui peuvent être utilisés avec les instructions de comparaison de chaînes.

La Figure 5.13 montre les deux nouveaux préfixes et décrit ce qu'ils font.

Image non disponible

REPE et REPZ sont simplement des synonymes pour le même préfixe (comme le sont REPNE et REPNZ). Si l'instruction de comparaison de chaînes répétée stoppe à cause du résultat de la comparaison, le ou les registres d'index sont quand même incrémentés et ECX décrémenté, cependant, le registre FLAGS contient toujours l'état qu'il avait à la fin de la répétition. Donc, il est possible d'utiliser le drapeau Z pour déterminer si la comparaison s'est arrêtée à cause de son résultat ou si c'est parce que ECX a atteint zéro.

Pourquoi ne peut-on pas simplement regarder si ECX est à zéro après la comparaison répétée ?

La Figure 5.14 montre un extrait de code qui détermine si deux blocs de mémoire sont égaux.

Image non disponible

Le JE de la ligne 7 de l'exemple vérifie le résultat de l'instruction précédente. Si la comparaison s'est arrêtée parce qu'elle a trouvé deux octets différents, le drapeau Z sera toujours éteint et aucun branchement n'est effectué, cependant, si la comparaison s'est arrêtée parce que ECX a atteint zéro, le drapeau Z sera toujours allumé et le code se branche à l'étiquette equal.

VI-B-5. Exemple

Cette section contient un fichier source assembleur avec plusieurs fonctions qui implémentent des opérations sur les tableaux en utilisant des instructions de chaînes. Beaucoup de ces fonctions font doublons avec des fonctions familières de la bibliothèque C.

memory.asm

Sélectionnez

1 global _asm_copy, _asm_find, _asm_strlen, _asm_strcpy
2
3 segment .text
4 ; fonction _asm_copy
5 ; copie deux blocs de mémoire
6 ; prototype C
7 ; void asm_copy( void * dest, const void * src, unsigned sz);
8 ; paramètres :
9 ; dest - pointeur sur le tampon vers lequel copier
10 ; src - pointeur sur le tampon depuis lequel copier
11 ; sz - nombre d'octets à copier
12
13 ; ci-dessous, quelques symboles utiles sont définis
14
15 ?fine dest [ebp+8]
16 ?fine src [ebp+12]
17 ?fine sz [ebp+16]
18 _asm_copy :
19 enter 0, 0
20 push esi
21 push edi
22
23 mov esi, src ; esi = adresse du tampon depuis lequel copier
24 mov edi, dest ; edi = adresse du tampon vers lequel copier
25 mov ecx, sz ; ecx = nombre d'octets à copier
26
27 cld ; éteint le drapeau de direction
28 rep movsb ; exécute movsb ECX fois
29
30 pop edi
31 pop esi
32 leave
33 ret
34
35
36 ; fonction _asm_find
37 ; recherche un octet donné en mémoire
38 ; void * asm_find( const void * src, char target, unsigned sz);
39 ; paramètres :
40 ; src - pointeur sur le tampon dans lequel chercher
41 ; target - octet à rechercher
42 ; sz - nombre d'octets dans le tampon
43 ; valeur de retour :
44 ; si l'élément recherché est trouvé, un pointeur vers sa première occurrence dans le tampon
45 ; est retourné
46 ; sinon, NULL est retourné
47 ; NOTE : l'élément à rechercher est un octet, mais il est empilé comme une valeur dword.
48 ; La valeur de l'octet est stockée dans les 8 bits de poids faible.
49 ;
50 ?fine src [ebp+8]
51 ?fine target [ebp+12]
52 ?fine sz [ebp+16]
53
54 _asm_find :
55 enter 0,0
56 push edi
57
58 mov eax, target ; al a la valeur recherchée
59 mov edi, src
60 mov ecx, sz
61 cld
62
63 repne scasb ; scanne jusqu'à ce que ECX == 0 ou [ES:EDI] == AL
64
65 je found_it ; si le drapeau zéro est allumé, on a trouvé
66 mov eax, 0 ; si pas trouvé, retourner un pointeur NULL
67 jmp short quit
68 found_it :
69 mov eax, edi
70 dec eax ; si trouvé retourner (DI - 1)
71 quit :
72 pop edi
73 leave
74 ret
75
76
77 ; fonction _asm_strlen
78 ; retourne la taille d'une chaîne
79 ; unsigned asm_strlen( const char * );
80 ; paramètre :
81 ; src - pointeur sur la chaîne
82 ; valeur de retour :
83 ; nombre de caractères dans la chaîne (sans compter le 0 terminal) (dans EAX)
84
85 ?fine src [ebp + 8]
86 _asm_strlen :
87 enter 0,0
88 push edi
89
90 mov edi, src ; edi = pointeur sur la chaîne
91 mov ecx, 0FFFFFFFFh ; utilise la plus grande valeur possible de ECX
92 xor al,al ; al = 0
93 cld
94
95 repnz scasb ; recherche le 0 terminal
96
97 ;
98 ; repnz ira un cran trop loin, donc la longueur vaut FFFFFFFE - ECX,
99 ; pas FFFFFFFF - ECX
100 ;
101 mov eax,0FFFFFFFEh
102 sub eax, ecx ; longueur = 0FFFFFFFEh - ecx
103
104 pop edi
105 leave
106 ret
107
108 ; fonction _asm_strcpy
109 ; copie une chaîne
110 ; void asm_strcpy( char * dest, const char * src);
111 ; paramètres :
112 ; dest - pointeur sur la chaine vers laquelle copier
113 ; src - pointeur sur la chaîne depuis laquelle copier
114 ;
115 ?fine dest [ebp + 8]
116 ?fine src [ebp + 12]
117 _asm_strcpy :
118 enter 0,0
119 push esi
120 push edi
121
122 mov edi, dest
123 mov esi, src
124 cld
125 cpy_loop:
126 lodsb ; charge AL & incrémente SI
127 stosb ; stocke AL & incrémente DI
128 or al, al ; positionne les drapeaux de condition
129 jnz cpy_loop ; si l'on n’est pas après le 0 terminal, on continue
130
131 pop edi
132 pop esi
133 leave
134 ret

memex.c

Sélectionnez

1 #include <stdio.h>
2
3 #dene STR_SIZE 30
4 / prototypes /
5
6 void asm_copy( void , const void , unsigned ) __attribute__((cdecl));
7 void asm_nd( const void ,
8 char target , unsigned ) __attribute__((cdecl));
9 unsigned asm_strlen( const char ) __attribute__((cdecl));
10 void asm_strcpy( char , const char ) __attribute__((cdecl));
11
12 int main()
13 {
14 char st1 [STR_SIZE] = "chaine test";
15 char st2 [STR_SIZE];
16 char st ;
17 char ch;
18
19 asm_copy(st2, st1, STR_SIZE); / copie les 30 caractères de la chaine /
20 printf ("%s\n", st2);
21
22 printf ("Entrez un è caractre : " ); / recherche un octet dans la îchane /
23 scanf("%c%[^\n]", &ch);
ProductID="24 st" w:st="on">24 st = asm_nd(st2, ch, STR_SIZE);
25 if ( st )
26 printf ("éTrouv : %s\n", st );
27 else
28 printf ("Pas étrouv\n");
29
30 st1 [0] = 0;
31 printf ("Entrez une îchane :");
32 scanf("%s", st1);
33 printf ("longueur = %u\n", asm_strlen(st1));
34
35 asm_strcpy( st2, st1 ); / copie des édonnes dans la îchane /
36 printf ("%s\n", st2 );
37
38 return 0;
39 }


VII. Virgule flottante

VII-A. Représentation en virgule flottante

VII-A-1. Nombres binaires non entiers

Lorsque nous avons parlé des systèmes numériques dans le premier chapitre, nous n'avons abordé que les nombres entiers. Évidemment, il est possible de représenter des nombres non entiers dans des bases autres que le décimal.

En décimal, les chiffres à droite du symbole décimal sont associés à des puissances

négatives de 10 :

0, 123 = 1 × 10-1 + 2 × 10-2 + 3 × 10-3

Ce n'est pas étonnant, les nombres binaires fonctionnent de la même façon :

0, 1012 = 1 × 2-1 + 0 × 2-2 + 1 × 2-3 = 0, 625

Cette idée peut être associée avec les méthodes appliquées aux entiers dans le Chapitre II pour convertir un nombre quelconque :

110, 0112 = 4 + 2 + 0, 25 + 0, 125 = 6, 375

Convertir du binaire vers le décimal n'est pas très difficile non plus. Divisez le nombre en deux parties : entière et décimale. Convertissez la partie entière en binaire en utilisant les méthodes du Chapitre II, la partie décimale est convertie en utilisant la méthode décrite ci-dessous.

Considérons une partie décimale binaire dont les bits sont notés a, b, c… Le nombre en binaire ressemble alors à :

0, abcdef…

Multipliez le nombre par deux. La représentation du nouveau nombre sera :

a, bcdef…

Notez que le premier bit est maintenant en tête. Remplacez le a par 0 pour obtenir :

0, bcdef…

et multipliez à nouveau par deux, vous obtenez :

b, cdef…

Maintenant le deuxième bit (b) est à la première place. Cette procédure peut être répétée jusqu'à ce qu'autant de bits que nécessaire soient trouvés.

La Figure 6.1 montre un exemple réel qui converti 0,5625 en binaire.

Image non disponible

La méthode s'arrête lorsque la partie décimale arrive à zéro.

Prenons un autre exemple, considérons la conversion de 23,85 en binaire, il est facile de convertir la partie entière (23 = 101112), mais qu'en est-il de la partie décimale (0, 85) ?

La Figure 6.2 montre le début de ce calcul.

Image non disponible Image non disponible

Une conséquence importante du calcul ci-dessus est que 23,85 ne peut pas être représenté exactement en binaire en utilisant un nombre fini de bits (tout comme 1/3 ne peut pas être représenté en décimal avec un nombre fini de chiffres). Comme le montre ce chapitre, les variables float et double en C sont stockées en binaire. Donc, les valeurs comme 23,85 ne peuvent pas être stockées exactement dans ce genre de variables. Seule une approximation de 23,85 peut être stockée.

Pour simplifier le matériel, les nombres en virgule flottante sont stockés dans un format standard. Ce format utilise la notation scientifique (mais en binaire, en utilisant des puissances de 2, pas de 10). Par exemple, 23,85 ou 10111, 11011001100110…2 serait stocké sous la forme :

1, 011111011001100110… × 2100

(où l'exposant (100) est en binaire). Un nombre en virgule flottante normalisé a la forme :

1, ssssssssssssssss × 2eeeeeee

où 1, sssssssssssss est la mantisse et eeeeeeee est l'exposant.

VII-A-2. Représentation en virgule flottante IEEE

L'IEEE (Institute of Electrical and Electronic Engineers) est une organisation internationale qui a conçu des formats binaires spécifiques pour stocker les nombres en virgule flottante. Ce format est utilisé sur la plupart des ordinateurs (mais pas tous !) fabriqués de nos jours. Souvent, il est supporté par le matériel de l'ordinateur lui-même. Par exemple, les coprocesseurs numériques (ou arithmétiques) d'Intel (qui sont intégrés à tous leurs processeurs depuis le Pentium) l'utilisent. L'IEEE définit deux formats différents avec des précisions différentes : simple et double précision. La simple précision est utilisée pour les variables float en C et la double précision pour les variables double.

Le coprocesseur arithmétique d'Intel utilise également une troisième précision plus élevée appelée précision étendue. En fait toutes les données dans le coprocesseur sont dans ce format. Lorsqu'elles sont stockées en mémoire depuis le coprocesseur, elles sont converties soit en simple, soit en double précision automatiquement (28). La précision étendue utilise un format général légèrement différent des formats float et double de l'IEEE, nous n'en parlerons donc pas ici.

VII-A-2-a. Simple précision IEEE

La virgule flottante simple précision utilise 32 bits pour encoder le nombre.

Elle est généralement précise jusqu'à 7 chiffres significatifs. Les nombres en virgule flottante sont généralement stockés dans un format beaucoup plus compliqué que celui des entiers.

La Figure 6.3 montre le format de base d'un nombre en simple précision IEEE.

Image non disponible

Ce format a plusieurs inconvénients. Les nombres en virgule flottante n'utilisent pas la représentation en complément à 2 pour les nombres négatifs. Ils utilisent une représentation en grandeur signée. Le bit 31 détermine le signe du nombre.

L'exposant binaire n'est pas stocké directement. À la place, la somme de l'exposant et de ProductID="7F" w:st="on">7F est stockée dans les bits 23 à 30, cet exposant décalé est toujours positif ou nul.

La partie décimale suppose une mantisse normalisée (de la forme 1, sssssssss).

Comme le premier bit est toujours un 1, le 1 de gauche n'est pas stocké ! Cela permet le stockage d'un bit additionnel à la fin et augmente donc légèrement la précision. Cette idée est appelée représentation en 1 masqué.

Il faut toujours garder à l'esprit que les octets 41 BE CC CD peuvent être interprétés de façons différentes selon ce qu'en fait le programme ! Vus comme un nombre en virgule flottante en simple précision, ils représentent 23,850000381, mais vus comme un entier double-mot, ils représentent 1,103,023,309 ! Le processeur ne sait pas quelle est la bonne interprétation !

Comment serait stocké 23,85 ? Tout d'abord, il est positif, donc le bit de signe est à 0. Ensuite, l'exposant réel est 4, donc l'exposant décalé est

ProductID="7F" w:st="on">7F + 4 = 8316.

Enfin, la fraction vaut 01111101100110011001100 (souvenez-vous que le 1 de tête est masqué). En les mettant bout à bout (pour clarifier les différentes sections du format en virgule flottante, le bit de signe et la fraction ont été soulignés et les bits ont été regroupés en groupes de 4 bits) :

Image non disponible

Cela ne fait pas exactement 23,85 (puisqu'il s'agit d'un binaire répétitif). Si l'on convertit le chiffre ci-dessus en décimal, on constate qu'il vaut approximativement 23,849998474, ce nombre est très proche de 23,85, mais n'est pas exactement le même. En réalité, en C, 23,85 ne serait pas représenté exactement comme ci-dessus. Comme le bit le plus à gauche qui a été supprimé de la représentation exacte valait 1, le dernier bit est arrondi à 1, donc 23,85 serait représenté par 41 BE CC CD en hexa en utilisant la simple précision. En décimal, ce chiffre vaut 23,850000381 ce qui est une meilleure approximation de 23,85.

Comment serait représenté -23,85 ? Il suffit de changer le bit de signe :

C1 BE CC CD. Ne prenez pas le complément à 2 !

Certaines combinaisons de e et f ont une signification spéciale pour les flottants IEEE. Le Tableau 6.1 décrit ces valeurs.

Image non disponible

Un infini est produit par un dépassement de capacité ou une division par zéro. Un résultat indéfini

est produit par une opération invalide comme rechercher la racine carrée d'un nombre négatif, additionner deux infinis, etc.

Les nombres en simple précision normalisés peuvent prendre des valeurs de

Image non disponible

VII-A-2-b. Nombres dénormalisés

Les nombres dénormalisés peuvent être utilisés pour représenter des nombres avec des grandeurs trop petites à normaliser (i.e. inférieures à 1, 0 × 2-126).

Par exemple, considérons le nombre Image non disponible

Dans la forme normalisée, l'exposant est trop petit. Par contre, il peut être représenté sous une forme non normalisée : 0, 010012 × 2-127. Pour stocker ce nombre, l'exposant décalé est positionné à 0 (voir Tableau 6.1) et la partie décimale est la mantisse complète du nombre écrite comme un produit avec 2-127 (i.e. tous les bits sont stockés, y compris le 1 à gauche du point décimal). La représentation de 1, 001 × 2-129 est donc :

Image non disponible

VII-A-2-c. Double précision IEEE

La double précision IEEE utilise 64 bits pour représenter les nombres et est précise jusqu'à environ 15 chiffres significatifs. Comme le montre la Figure 6.4, le format de base est très similaire à celui de la simple précision.

Image non disponible

Plus de bits sont utilisés pour l'exposant décalé (11) et la partie décimale (52).

La plage de valeurs plus grande pour l'exposant décalé a deux conséquences. La première est qu'il est calculé en faisant la somme de l'exposant réel et de 3FF (1023) (pas ProductID="7F" w:st="on">7F comme pour la simple précision). Deuxièmement, une grande plage d'exposants réels (et donc une plus grande plage de grandeurs) est disponible. Les grandeurs en double-précision peuvent prendre des valeurs entre environ 13-308 et 10308 .

C'est la plus grande taille du champ réservé à la partie décimale qui est reponsable de l'augmentation du nombre de chiffres significatifs pour les valeurs doubles.

Pour illustrer cela, reprenons 23,85. L'exposant décalé sera 4+3FF = 403 en hexa. Donc, la représentation double sera :

Image non disponible

ou 40 37 D9 99 99 99 99 9A en hexa. Si l'on reconvertit ce nombre en décimal, on trouve 23,8500000000000014 (il y a 12 zéros !) ce qui est une approximation de 23,85 nettement meilleure.

La double précision a les mêmes valeurs spéciales que la simple précision (29).

Les nombres dénormalisés sont également très similaires. La seule différence principale est que les nombres doubles dénormalisés utilisent 2-1023 au lieu de 2-127.

VII-B. Arithmétique en virgule flottante

L'arithmétique en virgule flottante sur un ordinateur est différente de celle des mathématiques. En mathématiques, tous les nombres peuvent être considérés comme exacts. Comme nous l'avons montré dans la section précédente, sur un ordinateur beaucoup de nombres ne peuvent pas être représentés exactement avec un nombre fini de bits. Tous les calculs sont effectués avec une précision limitée. Dans les exemples de cette section, des nombres avec une mantisse de 8 bits seront utilisés pour plus de simplicité.

VII-B-1. Addition

Pour additionner deux nombres en virgule flottante, les exposants doivent être égaux. S'ils ne le sont pas déjà, alors, il faut les rendre égaux en décalant la mantisse du nombre ayant le plus petit exposant. Par exemple, considérons

10, 375 + 6, 34375 = 16, 71875 ou en binaire :

Image non disponible

Ces deux nombres n'ont pas le même exposant, donc on décale la mantisse pour rendre les exposants égaux, puis on effectue l'addition :

Image non disponible

Notez que le décalage de 1, 1001011 × 22 supprime le 1 de tête et arrondi plus tard le résultat en 0, 1100110 × 23. Le résultat de l'addition, 10, 0001100 × 23 ou 1, 00001100 × 24), est égal à

10000,1102 ou 16,75, ce n'est pas égal à la réponse exacte (16,71875) ! Il ne s'agit que d'une approximation due aux erreurs d'arrondi du processus d'addition.

Il est important de réaliser que l'arithmétique en virgule flottante sur un ordinateur (ou une calculatrice) est toujours une approximation. Les lois des mathématiques ne fonctionnent pas toujours avec les nombres en virgule flottante sur un ordinateur. Les mathématiques supposent une précision infinie qu'aucun ordinateur ne peut atteindre. Par exemple, les mathématiques nous apprennent que (a + b) − b = a, cependant, ce n'est pas forcément exactement vrai sur un ordinateur !

VII-B-2. Soustraction

La soustraction fonctionne d'une façon similaire à l'addition et souffre des mêmes problèmes. Par exemple, considérons 16, 75 − 15, 9375 = 0, 8125 :

Image non disponible

Décaler 1, 1111111 × 23 donne (en arrondissant) 1, 0000000 × 24

Image non disponible

VII-B-3. Multiplication et division

Pour la multiplication, les mantisses sont multiplées et les exposants sont additionnés. Considérons 10, 375 × 2, 5 = 25, 9375 :

Image non disponible

Bien sûr, le résultat réel serait arrondi à 8 bits pour donner :

1, 1010000 × 24 = 11010, 0002 = 26

La division est plus compliquée, mais souffre des mêmes problèmes avec des erreurs d'arrondi.

VII-B-4. Conséquences sur la programmation

Le point essentiel de cette section est que les calculs en virgule flottante ne sont pas exacts. Le programmeur doit en être conscient. Une erreur courante que les programmeurs font avec les nombres en virgule flottante est de les comparer en supposant qu'un calcul est exact. Par exemple, considérons une fonction appelée f(x) qui effectue un calcul complexe et un programme qui essaie de trouver la racine de la fonction (30). On peut être tenté d'utiliser l'expression suivante pour vérifier si x est une racine :

Sélectionnez

if ( f(x) == 0.0 )

Mais, que se passe-t-il si f(x) retourne 1×10-30 ? Cela signifie très probablement que x est une très bonne approximation d'une racine réelle, cependant, l'égalité ne sera pas vérifiée. Il n'y a peut-être aucune valeur en virgule flottante IEEE de x qui renvoie exactement zéro, en raison des erreurs d'arrondi dans f(x).

Une meilleure méthode serait d'utiliser :

Sélectionnez

if ( fabs(f(x)) < EPS )

où EPS est une macro définissant une très petite valeur positive (du genre 1 x 10-10). Cette expression est vraie dès que f(x) est très proche de zéro.

En général, pour comparer une valeur en virgule flottante (disons x) à une autre (y), on utilise :

Sélectionnez

if ( fabs(x &#8722; y)/fabs(y) < EPS )

VII-C. Le coprocesseur arithmétique

VII-C-1. Matériel

Les premiers processeurs Intel n'avaient pas de support matériel pour les opérations en virgule flottante. Cela ne signifie pas qu'ils ne pouvaient pas effectuer de telles opérations. Cela signifie seulement qu'elles devaient être réalisées par des procédures composées de beaucoup d'instructions qui n'étaient pas en virgule flottante. Pour ces systèmes, Intel fournissait une puce appelée coprocesseur mathématique. Un coprocesseur mathématique a des instructions machine qui effectuent beaucoup d'opérations en virgule flottante beaucoup plus rapidement qu'en utilisant une procédure logicielle (sur les premiers processeurs, au moins 10 fois plus vite !). Le coprocesseur pour le 8086/8088 s'appelait le 8087, pour le 80286, il y avait un 80287 et pour le 80386, un 80387, le processeur 80486DX intégrait le coprocesseur mathématique dans le 80486 lui-même (31). Depuis le Pentium, toutes les générations de processeurs 80x86 ont un coprocesseur mathématique intégré. Cependant, on le programme toujours comme s'il s'agissait d'une unité séparée. Même les systèmes plus anciens sans coprocesseur peuvent installer un logiciel qui émule un coprocesseur mathématique. Cet émulateur est automatiquement activé lorsqu'un programme exécute une instruction du coprocesseur et lance la procédure logicielle qui produit le même résultat que celui qu'aurait donné le coprocesseur (bien que cela soit plus lent, bien sûr).

Le coprocesseur arithmétique a huit registres de virgule flottante. Chaque registre contient 80 bits de données. Les nombres en virgule flottante sont toujours stockés sous la forme de nombres 80 bits en précision étendue dans ces registres. Les registres sont appelés ST0, ST1, ST2… ST7. Les registres en virgule flottante sont utilisés différemment des registres entiers du processeur principal. Ils sont organisés comme une pile. Souvenez-vous qu'une pile est une liste Last-In First-Out (LIFO, dernier entré, premier sorti). ST0 fait toujours référence à la valeur au sommet de la pile. Tous les nouveaux nombres sont ajoutés au sommet de la pile. Les nombres existants sont décalés vers le bas pour faire de la place au nouveau.

Il y a également un registre de statut dans le coprocesseur arithmétique.

Il a plusieurs drapeaux. Seuls les 4 drapeaux utilisés pour les comparaisons seront traités : C0, C1, C2 et C3. Leur utilisation est traitée plus tard.

VII-C-2. Instructions

Pour faciliter la distinction entre les instructions du processeur normal et celles du coprocesseur, tous les mnémoniques du coprocesseur commencent par un F.

VII-C-2-a. Chargement et stockage

Il y a plusieurs instructions qui chargent des données au sommet de la pile du coprocesseur :

FLD source charge un nombre en virgule flottante depuis la mémoire vers

le sommet de la pile. La source peut être un nombre en simple,

double ou précision étendue ou un registre du coprocesseur.

FILD source lit un entier depuis la mémoire, le convertit en flottant et stocke le résultat au sommet de la pile. La source peut être un mot, un double-mot ou un quadruple-mot.

FLD1 Stocke un 1 au sommet de la pile.

FLDZ Stocke un zéro au sommet de la pile.

Il y a également plusieurs instructions qui déplacent les données depuis la pile vers la mémoire. Certaines de ces instructions retirent le nombre de la pile en même temps qu'elles le déplacent.

FST dest stocke le sommet de la pile (ST0) en mémoire. La destination peut être un nombre en simple ou double précision ou un registre du coprocesseur.

FSTP dest stocke le sommet de la pile en mémoire, exactement comme FST ; cependant, une fois le nombre stocké, sa valeur est retirée de la pile. La destination peut être un nombre en simple, double ou précision étendue ou un registre du coprocesseur.

FIST dest stocke la valeur au sommet de la pile convertie en entier en mémoire. La destination peut être un mot ou un double-mot. La pile en elle-même reste inchangée. La façon dont est converti le nombre en entier dépend de certains bits dans le mot de contrôle du coprocesseur. Il s'agit d'un registre d'un mot spécial (pas en virgule flottante) qui contrôle le fonctionnement du coprocesseur. Par défaut, le mot de contrôle est initialisé afin qu'il arrondisse à l'entier le plus proche lorsqu'il convertit vers un entier. Cependant, les instructions FSTCW (Store Control Word, stocker le mot de contrôle) et FLDCW (Load Control Word, charger le mot de contrôle) peuvent être utilisées pour changer ce comportement.

FISTP dest identique à FIST sauf en deux points. Le sommet de la pile est supprimé et la destination peut également être un quadruple-mot.

Il y a deux autres instructions qui peuvent placer ou retirer des données de la pile.

FXCH STn échange les valeurs de ST0 et STn sur la pile (où n est un numéro de registre entre 1 et 7).

FFREE STn libère un registre sur la pile en le marquant comme inutilisé ou vide.

VII-C-2-b. Addition et soustraction

Chacune des instructions d'addition calcule la somme de ST0 et d'un autre opérande. Le résultat est toujours stocké dans un registre du coprocesseur.

FADD source ST0 += source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FADD dest, ST0 dest += ST0. La destination peut être n'importe quel registre du coprocesseur.

FADDP dest ou FADDP dest, STO dest += ST0 puis ST0 est retiré de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIADD source ST0 += (float) source . Ajoute un entier à ST0. La source doit être un mot ou un double-mot en mémoire.

Il y a deux fois plus d'instructions pour la soustraction que pour l'addition, car l'ordre des opérandes est important pour la soustraction (i.e. a+b = b+a, mais a−b ≠ b−a !). Pour chaque instruction, il y a un miroir qui effectue la soustraction dans l'ordre inverse. Ces instructions inverses se finissent toutes soit par R soit par RP. La Figure 6.5 montre un court extrait de code qui ajoute les éléments d'un tableau de doubles.

Image non disponible

Au niveau des lignes 10 et 13, il faut spécifier la taille de l'opérande mémoire. Sinon l'assembleur ne saurait pas si l'opérande mémoire est un float (dword) ou un double (qword).

FSUB source ST0 -= source. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FSUBR source ST0 = source - ST0. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FSUB dest, ST0 dest -= ST0. La destination peut être n'importe quel registre du coprocesseur.

FSUBR dest, ST0 dest = ST0 - dest . La destination peut être n'importe quel registre du coprocesseur.

FSUBP dest ou FSUBP dest, STO dest -= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FSUBRP dest ou FSUBRP dest, ST0 dest = ST0 - dest puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FISUB source ST0 -= (float) source . Soustrait un entier de ST0. La source doit être un mot ou un double mot en mémoire.

FISUBR source ST0 = (float) source - ST0. Soustrait ST0 d'un entier. La source doit être un mot ou un double-mot en mémoire.

VII-C-2-c. Multiplication et division

Les instructions de multiplication sont totalement analogues à celles d'addition.

FMUL source ST0 *= source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FMUL dest, ST0 dest *= ST0. La destination peut être n'importe quel registre du coprocesseur.

FMULP dest ou FMULP dest, ST0 dest *= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIMUL source ST0 *= (float) source. Multiplie un entier avec ST0. La source peut être un mot ou un double-mot en mémoire.

Ce n'est pas étonnant, les instructions de division sont analogues à celles de soustraction. La division par zéro donne l'infini.

FDIV source ST0 /= source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FDIVR source ST0 = source / ST0. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FDIV dest, ST0 dest /= ST0. La destination peut être n'importe quel registre du coprocesseur.

FDIVR dest, ST0 dest = ST0 / dest . La destination peut être n'importe quel registre du coprocesseur.

FDIVP dest ou FDIVP dest, ST0 dest /= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FDIVRP dest ou FDIVRP dest, ST0 dest = ST0 / dest puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIDIV source ST0 /= (float) source. Divise ST0 par un entier. La src doit être un mot ou un double-mot en mémoire.

FIDIVR source ST0 = (float) source / ST0. Divise un entier par ST0. La source doit être un mot ou un double-mot en mémoire.

VII-C-2-d. Comparaisons

Le coprocesseur effectue également des comparaisons de nombres en virgule flottante. La famille d'instructions FCOM est faite pour ça.

FCOM source compare ST0 et source. La source peut être un registre du coprocesseur ou un float ou un double en mémoire.

FCOMP source compare ST0 et source, puis retire ST0 de la pile. La source peut être un registre du coprocesseur ou un float ou un double en mémoire.

FCOMPP compare ST0 et ST1, puis retire ST0 et ST1 de la pile.

FICOM source compare ST0 et (float) source. La source peut être un entier sur un mot ou un double-mot en mémoire.

FICOMP source compare ST0 et (float)source, puis retire ST0 de la pile. La source peut être un entier sur un mot ou un double-mot en mémoire.

FTST compare ST0 et 0,

Ces instructions changent les bits C0, C1, C2 et C3 du registre de statut du coprocesseur. Malheureusement, il n'est pas possible pour le processeur d'accéder à ces bits directement. Les instructions de branchement conditionnel utilisent le registre FLAGS, pas le registre de statut du coprocesseur.

Cependant, il est relativement simple de transférer les bits du mot de statut dans les bits correspondants du registre FLAGS en utilisant quelques instructions nouvelles :

FSTSW destination stocke le mot de statut du coprocesseur soit dans un mot en mémoire soit dans le registre AX.

SAHF stocke le registre AH dans le registre FLAGS.

LAHF charge le registre AH avec les bits du registre FLAGS.

La Figure 6.6 montre un court extrait de code en exemple.

Image non disponible

Les lignes 5 et 6 transfèrent les bits C0, C1, C2 et C3 du mot de statut du coprocesseur dans le registre FLAGS. Les bits sont transférés de façon à être identiques au résultat d'une comparaison de deux entiers non signés. C'est pourquoi la ligne 7 utilise une instruction JNA.

Les Pentium Pro (et les processeurs plus récents (Pentium II and III)) supportent deux nouveaux opérateurs de comparaison qui modifient directement le registre FLAGS du processeur.

FCOMI source compare ST0 et source. La source doit être un registre du coprocesseur.

FCOMIP source compare ST0 et source, puis retire ST0 de la pile. La source doit être un registre du coprocesseur.

La Figure 6.7 montre une sous-routine qui trouve le plus grand de deux double en utilisant l'instruction FCOMIP. Ne confondez pas ces instructions avec les fonctions de comparaisons avec un entier (FICOM et FICOMP).

Image non disponible

VII-C-2-e. Instructions diverses

Cette section traite de diverses autres instructions fournies par le coprocesseur.

FCHS ST0 = - ST0 change le signe de ST0.

FABS ST0 = |ST0| extrait la valeur absolue de ST0.

FSQRT extrait la racine carrée de ST0.

FSCALE ST0 = ST0 × 2[ST1] multiplie ST0 par une puissance de 2 rapidement. ST1 n'est pas retiré de la pile du coprocesseur.

La Figure 6.8 montre un exemple d'utilisation de cette instruction.

Image non disponible

VII-C-3. Exemple : formule quadratique

Le premier exemple montre comment la formule quadratique peut être codée en assembleur. Souvenez-vous, la formule quadratique calcule les solutions de l'équation :

ax2 + bx + c = 0

La formule donne deux solutions pour x : x1 et x2.

Image non disponible

L'expression sous la racine carrée (b2 − 4ac) est appelée le déterminant. Sa valeur est utile pour déterminer laquelle des trois possibilités suivantes est vérifiée pour les solutions :

1. Il n'y a qu'une seule solution double. b2 − 4ac = 0 ;

2. Il y a deux solutions réelles. b2 − 4ac > 0 ;

3. Il y a deux solutions complexes. b2 − 4ac < 0.

Voici un court programme C qui utilise la sous-routine assembleur :

quadt.c

Sélectionnez

2
3 int quadratic ( double, double, double, double , double );
4
5  int main()
6 {
7 double a,b,c , root1 , root2;
8
9 printf ("Entrez a , b, c : ");
10 scanf("%lf %lf %lf", &a, &b, &c);
11 if ( quadratic ( a , b, c, &root1, &root2 ) )
12 printf (" racines : %.10g %.10g\n", root1, root2 );
13 else
14 printf ("pas de racine réelle \n");
15 return 0;
16 }

Voici la routine assembleur :

quad.asm

Sélectionnez

1 ; fonction quadratic
2 ; trouve les solutions à l'équation quadratique :
3 ; a*x**2 + b*x + c = 0
4 ; prototype C :
5 ; int quadratic( double a, double b, double c,
6 ; double * root1, double *root2 )
7 ; Paramètres :
8 ; a, b, c - coefficients des puissances de l'équation quadratique (voir ci-dessus)
9 ; root1 - pointeur vers un double où stocker la première racine
10 ; root2 - pointeur vers un double où stocker la deuxième racine
11 ; Valeur de retour :
12 ; retourne 1 si des racines réelles sont trouvées, sinon 0
13
14 ?fine a qword [ebp+8]
15 ?fine b qword [ebp+16]
16 ?fine c qword [ebp+24]
17 ?fine root1 dword [ebp+32]
18 ?fine root2 dword [ebp+36]
19 ?fine disc qword [ebp-8]
20 ?fine one_over_2a qword [ebp-16]
21
22 segment .data
23 MinusFour dw -4
24
25 segment .text
26 global _quadratic
27 _quadratic:
28 push ebp
29 mov ebp, esp
30 sub esp, 16 ; alloue 2 doubles (disc & one_over_2a)
31 push ebx ; on doit sauvegarder l'ebx original
32
33 fild word [MinusFour]; pile : -4
34 fld a ; pile : a, -4
35 fld c ; pile : c, a, -4
36 fmulp st1 ; pile : a*c, -4
37 fmulp st1 ; pile : -4*a*c
38 fld b
39 fld b ; pile : b, b, -4*a*c
40 fmulp st1 ; pile : b*b, -4*a*c
41 faddp st1 ; pile : b*b - 4*a*c
42 ftst ; compare avec 0
43 fstsw ax
44 sahf
45 jb no_real_solutions ; si < 0, pas de solution réelle
46 fsqrt ; pile : sqrt(b*b - 4*a*c)
47 fstp disc ; stocke et décale la pile
48 fld1 ; pile : 1,0
49 fld a ; pile : a, 1,0
50 fscale ; pile : a * 2(1,0) = 2*a, 1
51 fdivp st1 ; pile : 1/(2*a)
52 fst one_over_2a ; pile : 1/(2*a)
53 fld b ; pile : b, 1/(2*a)
54 fld disc ; pile : disc, b, 1/(2*a)
55 fsubrp st1 ; pile : disc - b, 1/(2*a)
56 fmulp st1 ; pile : (-b + disc)/(2*a)
57 mov ebx, root1
58 fstp qword [ebx] ; stocke dans *root1
59 fld b ; pile : b
60 fld disc ; pile : disc, b
61 fchs ; pile : -disc, b
62 fsubrp st1 ; pile : -disc - b
63 fmul one_over_2a ; pile : (-b - disc)/(2*a)
64 mov ebx, root2
65 fstp qword [ebx] ; stocke dans *root2
66 mov eax, 1 ; la valeur de retour est 1
67 jmp short quit
68
69 no_real_solutions :
70 mov eax, 0 ; la valeur de retour est 0
71
72 quit :
73 pop ebx
74 mov esp, ebp
75 pop ebp
76 ret

VII-C-4. Exemple : lire un tableau depuis un fichier

Dans cet exemple, une routine assembleur lit des doubles depuis un fichier. Voici un court programme de test en C :

readt.c

Sélectionnez

1 /
2 Ce programme teste la procédure assembleur 32 bits read_doubles().
3 Il lit des doubles depuis stdin (utilisez une redirection pour lire depuis un fichier).
4 /
5 #include <stdio.h>
6 extern int read_doubles( FILE , double , int );
7 #dene MAX 100
8
9 int main()
10 {
11 int i ,n;
12 double a[MAX];
13
14 n = read_doubles(stdin, a , MAX);
15
16 for ( i=0; i < n; i++ )
17 printf ("= %g\n", i, a[i ]);
18 return 0;
19 }

Voici la routine assembleur :

read.asm

Sélectionnez

1 segment .data
2 format db "%lf", 0 ; format pour fscanf()
3,
4 segment .text
5 global _read_doubles
6 extern _fscanf
7
8 ?fine SIZEOF_DOUBLE 8
9 ?fine FP dword [ebp + 8]
10 ?fine ARRAYP dword [ebp + 12]
11 ?fine ARRAY_SIZE dword [ebp + 16]
12 ?fine TEMP_DOUBLE [ebp - 8]
13
14 ;
15 ; fonction _read_doubles
16 ; prototype C :
17 ; int read_doubles( FILE * fp, double * arrayp, int array_size );
18 ; Cette fonction lit des doubles depuis un fichier texte dans un tableau, jusqu'à
19 ; EOF ou que le tableau soit plein.
20 ; Paramètres :
21 ; fp - FILE pointeur à partir duquel lire (doit être ouvert en lecture)
22 ; arrayp - pointeur vers le tableau de double vers lequel lire
23 ; array_size - nombre d'éléments du tableau
24 ; Valeur de retour :
25 ; nombre de doubles stockés dans le tableau (dans EAX)
26
27 _read_doubles:
28 push ebp
29 mov ebp,esp
30 sub esp, SIZEOF_DOUBLE ; définit un double sur la pile
31
32 push esi ; sauve esi
33 mov esi, ARRAYP ; esi = ARRAYP
34 xor edx, edx ; edx = indice du tableau (initialement 0)
35
36 while_loop:
37 cmp edx, ARRAY_SIZE ; edx < ARRAY_SIZE ?
38 jnl short quit ; si non, quitte la boucle
39 ;
40 ; appelle fscanf() pour lire un double dans TEMP_DOUBLE
41 ; fscanf() peut changer edx, donc on le sauvegarde
42 ;
43 push edx ; sauve edx
44 lea eax, TEMP_DOUBLE
45 push eax ; empile &TEMP_DOUBLE
46 push dword format ; emplie &format
47 push FP ; emplie file pointer
48 call _fscanf
49 add esp, 12
50 pop edx ; restaure edx
51 cmp eax, 1 ; fscanf a retourné 1 ?
52 jne short quit ; si non, on quitte la boucle
53
54 ;
55 ; copie TEMP_DOUBLE dans ARRAYP[edx]
56 ; (Les 8 octets du double sont copiés en deux fois 4 octets)
57 ;
58 mov eax, [ebp - 8]
59 mov [esi + 8*edx], eax ; copie des 4 octets de poids faible
60 mov eax, [ebp - 4]
61 mov [esi + 8*edx + 4], eax ; copie des 4 octets de poids fort
62
63 inc edx
64 jmp while_loop
65
66 quit :
67 pop esi ; restaure esi
68
69 mov eax, edx ; stocke la valeur de retour dans eax
70
71 mov esp, ebp
72 pop ebp
73 ret

VII-C-5. Exemple : rechercher les nombres premiers

Ce dernier exemple recherche les nombres premiers, une fois de plus.

Cette implémentation est plus efficace que la précédente. Elle stocke les nombres premiers déjà trouvés dans un tableau et ne divise que par ceux-ci au lieu de diviser par tous les nombres impairs pour trouver de nouveaux premiers.

Une autre différence est qu'il calcule la racine carrée du candidat pour le prochain premier afin de déterminer le moment où il peut s'arrêter de rechercher des facteurs. Il altère le fonctionnement du coprocesseur afin que lorsqu'il stocke la racine sous forme d'entier, il la tronque au lieu de l'arrondir.

Ce sont les bits 10 et 11 du mot de contrôle qui permettent de paramétrer cela. Ces bits sont appelés les bits RC (Rounding Control, contrôle d'arrondi).

S'ils sont tous les deux à 0 (par défaut), le coprocesseur arrondit lorsqu'il convertit vers un entier. S'ils sont tous les deux à 1, le copresseur tronque les conversions en entiers. Notez que la routine fait attention à sauvegarder le mot de contrôle original et à le restaurer avant de quitter.

Voici le programme C pilote :

fprime.c

Sélectionnez

1 #include <stdio.h>
2 #include <stdlib.h>
3 /
4 fonction nd_primes
5 recherche le nombre indiqué de nombres premiers
6 èParamtres :
ProductID="7 a" w:st="on">7 a &#8722; tableau pour contenir les nombres premiers
8 n &#8722; nombre de nombres premiers à trouver
9 /
10 extern void nd_primes( int a , unsigned n );
11
12 int main()
13 {
14 int statut ;
15 unsigned i;
16 unsigned max;
17 int a;
18
19 printf ("Combien de nombres premiers voulez-vous trouver ? ");
20 scanf("%u", &max);
21
ProductID="22 a" w:st="on">22 a = calloc ( sizeof(int ), max);
23
24 if ( a ) {
25
26 nd_primes(a,max);
27
28 / affiche les 20 derniers nombres premiers trouvés /
29 for( i= ( max > 20 ) ? max &#8722; 20 : 0; i < max; i++ )
30 printf ("= %d\n", i+1, a[i]);
31
32 free (a);
33 statut = 0;
34 }
35 else {
36 fprintf ( stderr , "Impossible de  créer un tableau de %u entiers\n", max);
37 statut = 1;
38 }
39
40 return statut ;
41 }

Voici la routine assembleur :

prime2.asm

Sélectionnez

1 segment .text
2 global _find_primes
3 ;
4 ; fonction find_primes
5 ; trouve le nombre indiqué de nombres premiers
6 ; Paramètres :
7 ; array - tableau pour contenir les nombres premiers
8 ; n_find - nombre de nombres premiers à trouver
9 ; Prototype C :
10 ;extern void find_primes( int * array, unsigned n_find )
11 ;
12 ?fine array ebp + 8
13 ?fine n_find ebp + 12
14 ?fine n ebp - 4 ; Nombre de nombres premiers trouvés
15 ?fine isqrt ebp - 8 ; racine du candidat
16 ?fine orig_cntl_wd ebp - 10 ; mot de contrôle original
17 ?fine new_cntl_wd ebp - 12 ; nouveau mot de contrôle
18
19 _find_primes :
20 enter 12,0 ; fait de la place pour les variables locales
21
22 push ebx ; sauvegarde les variables registre éventuelles
23 push esi
24
25 fstcw word [orig_cntl_wd] ; récupère le mot de contrôle courant
26 mov ax, [orig_cntl_wd]
27 or ax, 0C00h ; positionne les bits d'arrondi à 11 (tronquer)
28 mov [new_cntl_wd], ax
29 fldcw word [new_cntl_wd]
30
31 mov esi, [array] ; esi pointe sur array
32 mov dword [esi], 2 ; array[0] = 2
33 mov dword [esi + 4], 3 ; array[1] = 3
34 mov ebx, 5 ; ebx = guess = 5
35 mov dword [n], 2 ; n = 2
36 ;
37 ; Cette boucle externe trouve un nouveau nombre premier à chaque itération qu'il
38 ; ajoute à la fin du tableau. Contrairement au programme de recherche de nombres
39 ; premier précédent, cette fonction ne détermine pas la primauté du nombre en
40 ; divisant par tous les nombres impairs. Elle ne divise que par les nombres
41 ; premiers qu'elle a déjà trouvé (c'est pourquoi ils sont stockés dans un tableau).
42 ;
43 while_limit:
44 mov eax, [n]
45 cmp eax, [n_find] ; while ( n < n_find )
46 jnb short quit_limit
47
48 mov ecx, 1 ; ecx est utilisé comme indice
49 push ebx ; stocke le candidat sur la pile
50 fild dword [esp] ; le charge sur la pile du coprocesseur
51 pop ebx ; retire le candidat de la pile
52 fsqrt ; calcule sqrt(guess)
53 fistp dword [isqrt] ; isqrt = floor(sqrt(quess))
54 ;
55 ; Cette boucle interne divise le candidat (ebx) par les nombres premiers
56 ; calculés précédemment jusqu'à ce qu'il trouve un de ses facteurs premiers
57 ; (ce qui signifie que le candidat n'est pas premier) ou jusqu'à ce que le
58 ; nombre premier par lequel diviser soit plus grand que floor(sqrt(guess))
59 ;
60 while_factor:
61 mov eax, dword [esi + 4*ecx] ; eax = array[ecx]
62 cmp eax, [isqrt] ; while ( isqrt < array[ecx]
63 jnbe short quit_factor_prime
64 mov eax, ebx
65 xor edx, edx
66 div dword [esi + 4*ecx]
67 or edx, edx ; && guess % array[ecx] != 0 )
68 jz short quit_factor_not_prime
69 inc ecx ; essaie le nombre premier suivant
70 jmp short while_factor
71
72 ;
73 ; found a new prime !
74 ;
75 quit_factor_prime :
76 mov eax, [n]
77 mov dword [esi + 4*eax], ebx ; ajoute le candidat au tableau
78 inc eax
79 mov [n], eax ; incrémente n
80
81 quit_factor_not_prime :
82 add ebx, 2 ; essaie le nombre impair suivant
83 jmp short while_limit
84
85 quit_limit:
86
87 fldcw word [orig_cntl_wd] ; restaure le mot de contrôle
88 pop esi ; restaure les variables registre
89 pop ebx
90
91 leave
92 ret


VIII-A. Structures et C++

VIII. Structures et C++

VIII-A. Structures

VIII-A-1. Introduction

Les structures sont utilisées en C pour regrouper des données ayant un rapport entre elles dans une variable composite. Cette technique a plusieurs avantages :

1. Cela clarifie le code en montrant que les données définies dans la structure sont intimement liées ;

2. Cela simplifie le passage des données aux fonctions. Au lieu de passer plusieurs variables séparément, elles peuvent être passées en une seule entité ;

3. Cela augmente la localité (32) du code.

Du point de vue de l'assembleur, une structure peut être considérée comme un tableau avec des éléments de tailles variables. Les éléments des vrais tableaux sont toujours de la même taille et du même type. C'est cette propriété qui permet de calculer l'adresse de n'importe quel élément en connaissant l'adresse de début du tableau, la taille des éléments et l'indice de l'élément voulu.

Les éléments d'une structure ne sont pas nécessairement de la même taille (et habituellement, ils ne le sont pas). À cause de cela, chaque élément d'une structure doit être explicitement spécifié et doit recevoir un tag (ou nom) au lieu d'un indice numérique.

En assembleur, on accède à un élément d'une structure d'une façon similaire à l'accès à un élément de tableau. Pour accéder à un élément, il faut connaître l'adresse de départ de la structure et le déplacement relatif de cet un tableau où ce déplacement peut être calculé grâce à l'indice de l'élément, c'est le compilateur qui affecte un déplacement aux éléments d'une structure.

Par exemple, considérons la structure suivante :

Sélectionnez

struct S {

short int x ; / entier sur 2 octets /

int y ; / entier sur 4 octets /

double z ; / ottant sur 8 octets /

};

La Figure 7.1 montre à quoi pourrait ressembler une variable de type S en mémoire.

Image non disponible

Le standard ANSI C indique que les éléments d'une structure sont organisés en mémoire dans le même ordre que celui de leur définition dans le struct. Il indique également que le premier élément est au tout début de la structure (i.e. au déplacement zéro). Il définit également une macro utile dans le fichier d'en-tête stddef.h appelée offsetof(). Cette macro calcule et renvoie le déplacement de n'importe quel élément d'une structure. La macro prend deux paramètres, le premier est le nom du type de la structure, le second est le nom de l'élément dont on veut le déplacement. Donc le résultat de offsetof(S, y) serait 2 d'après la Figure 7.1.

VIII-A-2. Alignement en mémoire

Si l'on utilise la macro offsetof pour trouver le déplacement de y en utilisant le compilateur gcc, on s'aperçoit qu'elle renvoie 4, pas 2 ! Pourquoi ?

Parce que gcc (et beaucoup d'autres compilateurs) aligne les variables sur des multiples de double-mot par défaut.

Souvenez-vous qu'une adresse est sur un multiple de double-mot si elle est divisible par 4.

En mode protégé 32 bits, le processeur lit la mémoire plus vite si la donnée commence sur un multiple de double-mot. La Figure 7.2 montre à quoi ressemble la structure S en utilisant gcc.

Image non disponible

Le compilateur insère deux octets inutilisés dans la structure pour aligner y (et z) sur un multiple de double-mot. Cela montre pourquoi c'est une bonne idée d'utiliser offsetof pour calculer les déplacements, au lieu de les calculer soi-même lorsqu'on utilise des structures en C.

Bien sûr, si la structure est utilisée uniquement en assembleur, le programmeur peut déterminer les déplacements lui-même. Cependant, si l'on interface du C et de l'assembleur, il est très important que l'assembleur et le C s'accordent sur les déplacements des éléments de la structure ! Une des complications est que des compilateurs C différents peuvent donner des déplacements différents aux éléments. Par exemple, comme nous l'avons vu, le compilateur gcc crée une structure S qui ressemble à la Figure 7.2, cependant, le compilateur de Borland créerait une structure qui ressemble à la Figure 7.1. Les compilateurs C fournissent le moyen de spécifier l'alignement utilisé pour les données. Cependant, le standard ANSI C ne spécifie pas comment cela doit être fait et donc, des compilateurs différents procèdent différemment.

Le compilateur gcc a une méthode flexible et compliquée de spécifier l'alignement. Le compilateur permet de spécifier l'alignement de n'importe quel type en utilisant une syntaxe spéciale. Par exemple, la ligne suivante :

Sélectionnez

typedef short int unaligned_int __attribute__((aligned(1)));

définit un nouveau type appelé unaligned_int qui est aligné sur des multiples d'octet (oui, toutes les parenthèses suivant __attribute__ sont nécessaires !) Le paramètre 1 de aligned peut être remplacé par d'autres puissances de 2 pour spécifier d'autres alignements (2 pour s'aligner sur les mots, 4 sur les doubles-mots, etc.). Si l'élément y de la structure était changé en un type unaligned_int, gcc placerait y au déplacement 2. Cependant, z serait toujours au déplacement 8 puisque les doubles sont également alignés sur des doubles-mots par défaut. La définition du type de z devrait aussi être changée pour le placer au déplacement 6.

Le compilateur gcc permet également de comprimer (pack) une structure.

Cela indique au compilateur d'utiliser le minimum d'espace possible pour la structure. La Figure 7.3 montre comment S pourrait être réécrite de cette façon. Cette forme de S utiliserait le moins d'octets possible, soit 14 octets.

Image non disponible

Les compilateurs de Microsoft et Borland supportent tous les deux la même méthode pour indiquer l'alignement par le biais d'une directive #pragma.

Sélectionnez

#pragma pack(1)

La directive ci-dessus indique au compilateur d'aligner les éléments des structures sur des multiples d'un octet (i.e., sans décalage superflu). Le 1 peut être remplacé par 2, 4, 8 ou 16 pour spécifier un alignement sur des multiples de mots, doubles-mots, quadruples-mots ou de paragraphe, respectivement. La directive reste active jusqu'à ce qu'elle soit écrasée par une autre. Cela peut poser des problèmes puisque ces directives sont souvent utilisées dans des fichiers d'en-tête. Si le fichier d'en-tête est inclus avant d'autres fichiers d'en-tête définissant des structures, ces structures peuvent être organisées différemment de ce qu'elles auraient été par défaut. Cela peut conduire à des erreurs très difficiles à localiser. Les différents modules d'un programme devraient organiser les éléments des structures à différents endroits !

Il y a une façon d'éviter ce problème. Microsoft et Borland permettent la sauvegarde de l'état de l'alignement courant et sa restauration. La Figure 7.4 montre comment on l'utilise.

Image non disponible

VIII-A-3. Champs de bits

Les champs de bits permettent de déclarer des membres d'une structure qui n'utilisent qu'un nombre de bits donné. La taille en bits n'a pas besoin d'être un multiple de 8. Un membre champ de bit est défini comme un unsigned int ou un int suivi de deux-points et de sa taille en bits. La Figure 7.5 en montre un exemple.

Image non disponible

Elle définit une variable 32 bits décomposée comme suit :

Image non disponible

Le premier champ de bits est assigné aux bits les moins significatifs du double-mot (33).

Néanmoins, le format n'est pas si simple si l'on observe comment les bits sont stockés en mémoire. La difficulté apparaît lorsque les champs de bits sont à cheval sur des multiples d'octets. Car les octets, sur un processeur little endian seront inversés en mémoire. Par exemple, les champs de bits de la structure S ressembleront à cela en mémoire :

Image non disponible

L'étiquette f2l fait référence aux cinq derniers bits (i.e., les cinq bits les moins significatifs) du champ de bits f2. L'étiquette f2m fait référence aux cinq bits les plus significatifs de f2. Les lignes verticales doubles montrent les limites d'octets. Si l'on inverse tous les octets, les morceaux des champs f2 et f3 seront réunis correctement.

L'organisation de la mémoire physique n'est habituellement pas importante à moins que des données soient transférées depuis ou vers le programme (ce qui est en fait assez courant avec les champs de bits). Il est courant que les interfaces de périphériques matériels utilisent des nombres impairs de bits dont les champs de bits facilitent la représentation.

Un bon exemple est SCSI (34). Une commande de lecture directe pour un périphérique SCSI est spécifiée en envoyant un message de six octets au périphérique selon le format indiqué dans la Figure 7.6.

Image non disponible

La difficulté de représentation en utilisant les champs de bits est l'adresse de bloc logique qui est à cheval sur trois octets différents de la commande. D'après la Figure 7.6, on constate que les données sont stockées au format big endian. La Figure 7.7 montre une définition qui essaie de fonctionner avec tous les compilateurs.

Image non disponible

Les deux premières lignes définissent une macro qui est vraie si le code est compilé avec un compilateur Borland ou Microsoft. La partie qui peut porter à confusion va des lignes 11 à 14. Tout d'abord, on peut se demander pourquoi les champs lba_mid et lba_lsb sont définis séparément et non pas comme un champ unique de 16 bits. C'est parce que les données sont stockées au format big endian. Un champ de 16 bits serait stocké au format little endian par le compilateur. Ensuite, les champs lba_msb et logical_unit semblent être inversés, cependant, ce n'est pas le cas. Ils doivent être placés dans cet ordre. La Figure 7.8 montre comment les champs sont organisés sous la forme d'une entité de 48 bits (les limites d'octets sont là encore représentées par des

lignes doubles).

Image non disponible

Lorsqu'elle est stockée en mémoire au format little endian, les bits sont réarrangés au format voulu (Figure 7.6).

Pour compliquer encore plus le problème, la définition de SCSI_read_cmd ne fonctionne pas correctement avec le C Microsoft. Si l'expression sizeof(SCSI_read_cmd) est évaluée, le C Microsoft renvoie 8 et non pas 6 ! C'est parce que le compilateur Microsoft utilise le type du champ de bits pour déterminer comment organiser les bits. Comme tous les bits sont déclarés comme unsigned, le compilateur ajoute deux octets à la fin de la structure pour qu'elle comporte un nombre entier de doubles-mots. Il est possible d'y remédier en déclarant tous les champs unsigned short. Maintenant, le compilateur Microsoft n'a plus besoin d'ajouter d'octets d'alignement puisque six octets forment un nombre entier de mots de deux octets (35). Les autres compilateurs fonctionnent également correctement avec ce changement. La Figure 7.9 montre une autre définition qui fonctionne sur les trois compilateurs.

Image non disponible

Elle ne déclare plus que deux champs de bits en utilisant le type unsigned char.

Le lecteur ne doit pas se décourager s'il trouve la discussion ci-dessus confuse. C'est confus ! L'auteur trouve souvent moins confus d'éviter d'utiliser des champs de bits en utilisant des opérations niveau bit pour examiner et modifier les bits manuellement.

VIII-A-4. Utiliser des structures en assembleur

Comme nous l'avons dit plus haut, accéder à une structure en assembleur ressemble beaucoup à accéder à un tableau. Prenons un exemple simple, regardons comment l'on pourrait écrire une routine assembleur qui mettrait à zéro l'élément y d'une structure S. Supposons que le prototype de la routine soit :

Sélectionnez

void zero_y( S s_p );

La routine assembleur serait :

Sélectionnez

1 ?fine y_offset 4
2 _zero_y :
3 enter 0,0
4 mov eax, [ebp + 8] ; récupère s_p depuis la pile
5 mov dword [eax + y_offset], 0
6 leave
7 ret

Le C permet de passer une structure par valeur à une fonction. Cependant, c'est une mauvaise idée la plupart du temps. Toutes les données de la structure doivent être copiées sur la pile puis récupérées par la routine. Il est beaucoup plus efficace de passer un pointeur vers la structure à la place.

Le C permet aussi qu'une fonction renvoie une structure. Évidemment, une structure ne peut pas être retournée dans le registre EAX. Des compilateurs différents gèrent cette situation de façon différente. Une situation courante que les compilateurs utilisent est de réécrire la fonction en interne de façon à ce qu'elle prenne un pointeur sur la structure en paramètre. Le pointeur est utilisé pour placer la valeur de retour dans une structure définie en dehors de la routine appelée.

La plupart des assembleurs (y compris NASM) ont un support intégré pour définir des structures dans votre code assembleur. Reportez-vous à votre documentation pour plus de détails.

VIII-B. Assembleur et C++

Le langage de programmation C++ est une extension du langage C. Beaucoup des règles valables pour interfacer le C et l'assembleur s'appliquent également au C++. Cependant, certaines règles doivent être modifiées. De plus, certaines extensions du C++ sont plus faciles à comprendre en connaissant le langage assembleur. Cette section suppose une connaissance basique du C++.

VIII-B-1. Surcharge et décoration de noms

Le C++ permet de définir des fonctions (et des fonctions membres) différentes avec le même nom. Lorsque plus d'une fonction partage le même nom, les fonctions sont dites surchargées. Si deux fonctions sont définies avec le même nom en C, l'éditeur de liens produira une erreur, car il trouvera deux définitions pour le même symbole dans les fichiers objet qu'il est en train de lier. Par exemple, prenons le code de la Figure 7.10.

Image non disponible

Le code assembleur équivalent définirait deux étiquettes appelées _f ce qui serait bien sûr une

erreur. Le C++ utilise le même procédé d'édition de liens que le C, mais évite cette erreur en effectuant une décoration de nom (name mangling) ou en modifiant le symbole utilisé pour nommer une fonction. D'une certaine façon, le C utilise déjà la décoration de nom. Il ajoute un caractère de soulignement au nom de la fonction C lorsqu'il crée l'étiquette pour la fonction. Cependant, il décorera le nom des deux fonctions de la Figure 7.10 de la même façon et produira une erreur. Le C++ utilise un procédé de décoration plus sophistiqué qui produit deux étiquettes différentes pour les fonctions. Par exemple, la première fonction de la Figure 7.10 recevrait l'étiquette _f__Fi et la seconde, _f__Fd, sous DJGPP. Cela évite toute erreur d'édition de liens.

Malheureusement, il n'y a pas de standard sur la gestion des noms en C++ et des compilateurs différents décorent les noms de façon différente.

Par exemple, Borland C++ utiliserait les étiquettes @f$qi et @f$qd pour les deux fonctions de la Figure 7.10. Cependant, les règles ne sont pas totalement arbitraires. Le nom décoré encode la signature de la fonction. La signature d'une fonction est donnée par l'ordre et le type de ses paramètres. Notez que la fonction qui ne prend qu'un argument int a un i à la fin de son nom décoré (à la fois sous DJGPP et Borland) et que celle qui prend un argument double a un d à la fin de son nom décoré. S'il y avait une fonction appelée f avec le prototype suivant :

Sélectionnez

void f ( int x , int y , double z);

DJGPP décorerait son nom en _f__Fiid et Borland en @f$qiid.

Le type de la fonction ne fait pas partie de la signature d'une fonction et n'est pas encodé dans le nom décoré. Ce fait explique une règle de la surcharge en C++. Seules les fonctions dont les signatures sont uniques peuvent être surchargées. Comme on le voit, si deux fonctions avec le même nom et la même signature sont définies en C++, elles donneront le même nom décoré et créeront une erreur lors de l'édition de liens. Par défaut, toutes les fonctions C++ sont décorées, même celles qui ne sont pas surchargées. Lorsqu'il compile un fichier, le compilateur n'a aucun moyen de savoir si une fonction particulière est surchargée ou non, il décore donc tous les noms. En fait, il décore également les noms des variables globales en encodant le type de la variable d'une façon similaire à celle utilisée pour les signatures de fonctions.

Donc, si l'on définit une variable globale dans un fichier avec un certain type puis que l'on essaie de l'utiliser dans un autre fichier avec le mauvais type, l'éditeur de liens produira une erreur. Cette caractéristique du C++ est connue sous le nom de typesafe linking (édition de liens avec respect des types) . Cela crée un autre type d'erreurs, les prototypes inconsistants. Cela arrive lorsque la définition d'une fonction dans un module ne correspond pas avec le prototype utilisé par un autre module. En C, cela peut être un problème très difficile à corriger. Le C ne détecte pas cette erreur. Le programme compilera et sera lié, mais aura un comportement imprévisible, car le code appelant placera sur la pile des types différents de ceux que la fonction attend. En C++ cela produira une erreur lors de l'édition de liens.

Lorsque le compilateur C++ analyse un appel de fonction, il recherche la fonction correspondante en observant les arguments qui lui sont passés (36).

S'il trouve une correspondance, il crée un CALL vers la fonction adéquate en utilisant les règles de décoration du compilateur.

Comme des compilateurs différents utilisent différentes règles de décoration de nom, il est possible que des codes C++ compilés par des compilateurs différents ne puissent pas être liés ensemble. C'est important lorsque l'on a l'intention d'utiliser une bibliothèque C++ précompilée ! Si l'on veut écrire une fonction en assembleur qui sera utilisée avec du code C++, il faut connaître les règles de décoration de nom du compilateur C++ utilisé (ou utiliser la technique expliquée plus bas).

L'étudiant astucieux pourrait se demander si le code de la Figure 7.10 fonctionnera de la façon attendue. Comme le C++ décore toutes les fonctions, alors la fonction printf sera décorée et le compilateur ne produira pas un CALL à l'étiquette _printf. C'est une question pertinente. Si le prototype de printf était simplement placé au début du fichier, cela arriverait. Son prototype est :

Sélectionnez

int printf ( const char , ...);

DJGPP décorerait ce nom en _printf__FPCce (F pour fonction, P pour pointeur, C pour const, c pour char et e pour ellipse). Cela n'appellerait pas la fonction printf de la bibliothèque C standard ! Bien sûr, il doit y avoir un moyen pour que le C++ puisse appeler du code C. C'est très important, car il existe une énorme quantité de vieux code C utile. En plus de permettre l'accès au code hérité de C, le C++ permet également d'appeler du code assembleur en utilisant les conventions de décoration standard du C.

Le C++ étend le mot-clé extern pour lui permettre de spécifier que la fonction ou la variable globale qu'il modifie utilise les conventions C normales.

Dans la terminologie C++, la fonction ou la variable globale utilise une édition de liens C. Par exemple, pour déclarer la fonction printf comme ayant une édition de liens C, utilisez le prototype :

Sélectionnez

extern "C" int printf ( const char , ... );

Cela impose au compilateur de ne pas utiliser les règles de décoration de nom du C++ sur la fonction, mais d'utiliser les règles C à la place. Cependant, en faisant cela, la fonction printf ne peut pas être surchargée. Cela constitue la façon la plus simple d'interfacer du C++ et de l'assembleur, définir une fonction comme utilisant une édition de liens C puis utiliser la convention d'appel C.

Pour plus de facilité, le C++ permet également de définir une édition de liens C sur un bloc de fonctions et de variables globales. Le bloc est indiqué en utilisant les accolades habituelles.

Sélectionnez

extern "C" {

/ variables globales et prototypes des fonctions ayant une édition de liens C /

}

Si l'on examine les fichiers d'en-tête ANSI C fournis avec les compilateurs C/C++ actuels, on trouve ce qui suit vers le début de chaque fichier d'entête :

Sélectionnez

#ifdef __cplusplus

extern "C" {

#endif

Et une construction similaire, vers la fin, contenant une accolade fermante.

Les compilateurs C++ définissent la macro __cplusplus (avec deux caractères de soulignement au début). L'extrait ci-dessus entoure tout le fichier d'en-tête dans un bloc extern "C" si le fichier d'en-tête est compilé en C++, mais ne fait rien s'il est compilé en C (puisqu'un compilateur C génèrerait une erreur de syntaxe sur extern "C"). La même technique peut être utilisée par n'importe quel programmeur pour créer un fichier d'en-tête pour des routines assembleur pouvant être utilisées en C ou en C++.

VIII-B-2. Références

Les références sont une autre nouvelle fonctionnalité du C++. Elles permettent de passer des paramètres à une fonction sans utiliser explicitement de pointeur. Par exemple, considérons le code de la Figure 7.11.

Image non disponible

En fait, les paramètres par référence sont plutôt simples, ce sont des pointeurs. Le compilateur masque simplement ce fait aux yeux du programmeur (exactement de la même façon que les compilateurs Pascal qui implémentent les paramètres var comme des pointeurs). Lorsque le compilateur génère l'assembleur pour l'appel de fonction ligne 7, il passe l'adresse de y. Si l'on écrivait la fonction f en assembleur, on ferait comme si le prototype était (37) :

Sélectionnez

void f ( int xp);

Les références sont juste une facilité qui est particulièrement utile pour la surcharge d'opérateurs. C'est une autre fonctionnalité du C++ qui permet de donner une signification aux opérateurs de base lorsqu'ils sont appliqués à des structures ou des classes. Par exemple, une utilisation courante est de définir l'opérateur plus (+) de façon à ce qu'il concatène les objets string.

Donc, si a et b sont des strings, a + b renverra la concaténation des chaînes a et b. Le C++ appellerait en réalité une fonction pour ce faire (en fait, cette expression pourrait être réécrite avec une notation fonction sous la forme operator +(a,b)). Pour plus d'efficacité, il est souhaitable de passer l'adresse des objets string à la place de les passer par valeur. Sans référence, cela pourrait être fait en écrivant operator +(&a,&b), mais cela imposerait d'écrire l'opérateur avec la syntaxe &a + &b. Cela serait très maladroit et confus. Par contre, en utilisant les références, il est possible d'écrire a + b, ce qui semble très naturel.

VIII-B-3. Fonctions inline

Les fonctions inline sont encore une autre fonctionnalité du C++ (38). Les fonctions inline sont destinées à remplacer les macros du préprocesseur qui prennent des paramètres, sources d'erreur. Sourvenez-vous en C, une macro qui élève un nombre au carré ressemble à cela :

Sélectionnez

#define SQR(x) ((x)(x))

Comme le préprocesseur ne comprend pas le C et ne fait que de simples substitutions, les parenthèses sont requises pour calculer le résultat correct dans la plupart des cas. Cependant, même cette version ne donnerait pas la bonne réponse pour SQR(x++).

Les macros sont utilisées, car elles éliminent la surcharge d'un appel pour une fonction simple. Comme le chapitre sur les sous-programmes l'a démontré, effectuer un appel de fonction implique plusieurs étapes. Pour une fonction très simple, le temps passé à l'appeler peut être plus grand que celui passé dans la fonction ! Les fonctions inline sont une façon beaucoup plus pratique d'écrire du code qui ressemble à une fonction, mais qui n'effectue pas de CALL à un bloc commun. Au lieu de cela, les appels à des fonctions inline sont remplacés par le code de la fonction. Le C++ permet de rendre une fonction inline en plaçant le mot-clé inline au début de sa définition.

Par exemple, considérons les fonctions déclarées dans la Figure 7.12.

Image non disponible

L'appel à la fonction f, ligne 10, est un appel de fonction normal (en assembleur, en supposant que x est à l'adresse ebp-8 et y en ebp-4) :

Sélectionnez

1 push dword [ebp-8]

2 call _f

3 pop ecx

4 mov [ebp-4], eax

Cependant, l'appel à la fonction inline_f, ligne 11 ressemblerait à :

Sélectionnez

1 mov eax, [ebp-8]

2 imul eax, eax

3 mov [ebp-4], eax

Dans ce cas, il y a deux avantages à inliner. Tout d'abord, la fonction inline est plus rapide. Aucun paramètre n'est placé sur la pile, aucun cadre de pile n'est créé puis détruit, aucun branchement n'est effectué. Ensuite, l'appel à la fonction inline utilise moins de code ! Ce dernier point est vrai pour cet exemple, mais ne reste pas vrai dans tous les cas.

La principal inconvénient de l'inlining est que le code inline n'est pas lié et donc le code d'une fonction inline doit être disponible pour tous les fichiers qui l'utilisent. L'exemple de code assembleur précédent le montre. L'appel de la fonction non-inline ne nécessite que la connaissance des paramètres, du type de valeur de retour, de la convention d'appel et du nom de l'étiquette de la fonction. Toutes ces informations sont disponibles par le biais du prototype de la fonction. Cependant, l'utilisation de la fonction inline nécessite la connaissance de tout le code de la fonction. Cela signifie que si n'importe quelle partie de la fonction change, tous les fichiers source qui utilisent la fonction doivent être recompilés. Souvenez-vous que pour les fonctions non inline, si le prototype ne change pas, souvent les fichiers qui utilisent la fonction n'ont pas besoin d'être recompilés. Pour toutes ces raisons, le code des fonctions inline est généralement placé dans les fichiers d'en-tête. Cette pratique est contraire à la règle stricte habituelle du C selon laquelle on ne doit jamais placer de code exécutable dans les fichiers d'en-tête.

VIII-B-4. Classes

Une classe C++ décrit un type d'objet. Un objet possède à la fois des membres données et des membres fonctions (39). En d'autres termes, il s'agit d'une structure à laquelle sont associées des données et des fonctions. Considérons la classe simple dénie dans la Figure 7.13.

Image non disponible

Une variable de type Simple ressemblerait à une structure C normale avec un seul membre int.

Les fonctions ne sont pas affectées à la structure en mémoire. Cependant, les fonctions membres sont différentes des autres fonctions. On leur passe un paramètre caché. Ce paramètre est un pointeur vers l'objet sur lequel agit la fonction.

En fait, le C++ utilise le mot-clé this pour accéder au pointeur vers l'objet lorsque l'on se trouve à l'intérieur d'une fonction membre.

Par exemple, considérons la méthode set_data de la classe Simple de la Figure 7.13. Si elle était écrite en C, elle ressemblerait à une fonction à laquelle on passerait explicitement un pointeur vers l'objet sur lequel elle agit comme le montre le code de la Figure 7.14.

Image non disponible

L'option -S du compilateur DJGPP (et des compilateurs gcc et Borland également) indique au compilateur de produire un fichier assembleur contenant l'équivalent assembleur du code. Pour DJGPP et gcc le fichier assembleur possède une extension .s et utilise malheureusement la syntaxe du langage assembleur AT&T qui est assez différente des syntaxes NASM et MASM (40) (les compilateurs Borland et MS génèrent un fichier avec l'extension .asm utilisant la syntaxe MASM). La Figure 7.15 montre la sortie de DJGPP convertie en syntaxe NASM avec des commentaires supplémentaires pour clarifier le but des instructions.

Image non disponible

Sur la toute première ligne, notez que la méthode set_data reçoit une étiquette décorée qui encode le nom de la méthode, le nom de la classe et les paramètres.

Le nom de la classe est encodé, car d'autres classes peuvent avoir une méthode appelée set_data et les deux méthodes doivent recevoir des étiquettes différentes. Les paramètres sont encodés afin que la classe puisse surcharger la méthode set_data afin qu'elle prenne d'autres paramètres, comme les fonctions C++ normales. Cependant, comme précédemment, des compilateurs différents encoderont ces informations différemment dans l'étiquette.

Ensuite, aux lignes 2 et 3 nous retrouvons le prologue habituel. À la ligne 5, le premier paramètre sur la pile est stocké dans EAX. Ce n'est pas le paramètre x ! Il s'agit du paramètre caché (41) qui pointe vers l'objet sur lequel on agit.

La ligne 6 stocke le paramètre x dans EDX et la ligne 7 stocke EDX dans le double-mot sur lequel pointe EAX. Il s'agit du membre data de l'objet Simple sur lequel on agit, qui, étant la seule donnée de la classe, est stocké au déplacement 0 de la structure Simple.

VIII-B-4-a. Exemple

Cette section utilise les idées de ce chapitre pour créer une classe C++ qui représente un entier non signé d'une taille arbitraire. Comme l'entier peut faire n'importe quelle taille, il sera stocké dans un tableau d'entiers non signés (doubles-mots). Il peut faire n'importe quelle taille en utilisant l'allocation dynamique. Les doubles-mots sont stockés dans l'ordre inverse (42) (i.e. le double-mot le moins significatif est au déplacement 0). La Figure 7.16 montre la définition de la classe Big_int (43).

Image non disponible

Fig. 7.16 Dénition de la classe Big_int

La taille d'un Big_int est mesurée par la taille du tableau d'unsigned utilisé pour stocker les données.

La donnée membre size_ de la classe est affectée au déplacement 0 et le membre number_ est affecté au déplacement 4.

Pour simplifier l'exemple, seuls les objets ayant des tableaux de la même taille peuvent être additionnés entre eux.

La classe a trois constructeurs : le premier (ligne 9) initialise l'instance de la classe en utilisant un entier non signé normal, le second (ligne 19) initalise l'instance en utilisant une chaîne qui contient une valeur hexadécimale. Le troisième constructeur (ligne 22) est le constructeur par copie.

Cette explication se concentre sur la façon dont fonctionnent les opérateurs d'addition et de soustraction, car c'est là que l'on utilise de l'assembleur.

La Figure 7.17 montre les parties du fichier d'en-tête relatives à ces opérateurs.

Image non disponible

Elles montrent comment les opérateurs sont paramétrés pour appeler des routines assembleur. Comme des compilateurs différents utilisent des règles de décoration radicalement différentes pour les fonctions opérateur, des fonctions opérateur inline sont utilisées pour initialiser les appels aux routines assembleur liées au format C. Cela les rend relativement simples à porter sur des compilateurs différents et est aussi rapide qu'un appel direct.

Cette technique élimine également le besoin de soulever une exception depuis l'assembleur !

Pourquoi l'assembleur n'est-il utilisé qu'ici ? Souvenez-vous que pour effectuer de l'arithmétique en précision multiple, la retenue doit être ajoutée au double mot significatif suivant. Le C++ (et le C) ne permet pas au programmeur d'accéder au drapeau de retenue du processeur. On ne pourrait effectuer l'addition qu'en recalculant indépendamment en C++ la valeur du drapeau de retenue et en l'ajoutant de façon conditionnelle au double-mot suivant. Il est beaucoup plus efficace d'écrire le code en assembleur à partir duquel on peut accéder au drapeau de retenue et utiliser l'instruction ADC qui ajoute automatiquement le drapeau de retenue.

Par concision, seule la routine assembleur add_big_ints sera expliquée ici. Voici le code de cette routine (contenu dans big_math.asm) :

big_math.asm

Sélectionnez

1 segment .text
2 global add_big_ints, sub_big_ints
3 ?fine size_offset 0
4 ?fine number_offset 4
5
6 ?fine EXIT_OK 0
7 ?fine EXIT_OVERFLOW 1
8 ?fine EXIT_SIZE_MISMATCH 2
9
10 ; Paramètres des routines add et sub
11 ?fine res ebp+8
12 ?fine op1 ebp+12
13 ?fine op2 ebp+16
14
15 add_big_ints:
16 push ebp
17 mov ebp, esp
18 push ebx
19 push esi
20 push edi
21 ;
22 ; initialise esi pour pointer vers op1
23 ; edi pour pointer vers op2
24 ; ebx pour pointer vers res
25 mov esi, [op1]
26 mov edi, [op2]
27 mov ebx, [res]
28 ;
29 ; s'assure que les 3 Big_int ont la même taille
30 ;
31 mov eax, [esi + size_offset]
32 cmp eax, [edi + size_offset]
33 jne sizes_not_equal ; op1.size_ != op2.size_
34 cmp eax, [ebx + size_offset]
35 jne sizes_not_equal ; op1.size_ != res.size_
36
37 mov ecx, eax ; ecx = taille des Big_int
38 ;
39 ; initialise les registres pour qu'ils pointent vers leurs tableaux respectifs
40 ; esi = op1.number_
41 ; edi = op2.number_
42 ; ebx = res.number_
43 ;
44 mov ebx, [ebx + number_offset]
45 mov esi, [esi + number_offset]
46 mov edi, [edi + number_offset]
47
48 clc ; met le drapeau de retenue à 0
49 xor edx, edx ; edx = 0
50 ;
51 ; boucle d'addition
52 add_loop:
53 mov eax, [edi+4*edx]
54 adc eax, [esi+4*edx]
55 mov [ebx + 4*edx], eax
56 inc edx ; ne modifie pas le drapeau de retenue
57 loop add_loop
58
59 jc overflow
60 ok_done:
61 xor eax, eax ; valeur de retour = EXIT_OK
62 jmp done
63 overflow:
64 mov eax, EXIT_OVERFLOW
65 jmp done
66 sizes_not_equal:
67 mov eax, EXIT_SIZE_MISMATCH
68 done:
69 pop edi
70 pop esi
71 pop ebx
72 leave
73 ret

Heureusement, la majorité de ce code devrait être compréhensible pour le lecteur maintenant. Les lignes 25 à 27 stockent les pointeurs vers les objets Big_int passés à la fonction via des registres. Souvenez-vous que les références sont des pointeurs. Les lignes 31 à 35 vérifient que les tailles des trois objets sont les mêmes (notez que le déplacement de size_ est ajouté au pointeur pour accéder à la donnée membre). Les lignes 44 à 46 ajustent les registres pour qu'ils pointent vers les tableaux utilisés par leurs objets respectifs au lieu des objets eux-mêmes (là encore, le déplacement du membre number_ est ajouté au pointeur sur l'objet).

La boucle des lignes 52 à 57 additionne les entiers stockés dans les tableaux en additionnant le double-mot le moins significatif en premier, puis les doubles-mots suivants, etc. L'addition doit être effectuée dans cet ordre pour l'arithmétique en précision étendue (voir Section III.A.5).

La ligne 59 vérifie qu'il n'y a pas de dépassement de capacité, lors d'un dépassement de capacité, le drapeau de retenue sera allumé par la dernière addition du double-mot le plus significatif. Comme les doubles-mots du tableau sont stockés dans l'ordre little endian, la boucle commence au début du tableau et avance jusqu'à la fin.

La Figure 7.18 montre un court exemple utilisant la classe Big_int.

Image non disponible

Notez que les constantes de Big_int doivent être déclarées explicitement comme à la ligne ProductID="16. C" w:st="on">16. C'est nécessaire pour deux raisons. Tout d'abord, il n'y a pas de constructeur de conversion qui convertisse un entier non signé en Big_int. Ensuite, seuls les Big_int de même taille peuvent être additionnés. Cela rend la conversion problématique puisqu'il serait difficile de savoir vers quelle taille convertir. Une implémentation plus sophistiquée de la classe permettrait d'additionner n'importe quelle taille avec n'importe quelle autre. L'auteur ne voulait pas compliquer inutilement cet exemple en l'implémentant ici (cependant, le lecteur est encouragé à le faire).

VIII-B-5. Héritage et polymorphisme

L'héritage permet à une classe d'hériter des données et des méthodes d'une autre. Par exemple, considérons le code de la Figure 7.19.

Image non disponible

Il montre deux classes, A et B, où la classe B hérite de A. La sortie du programme est :

Sélectionnez

Taille de a : 4 Déplacement de ad : 0

Taille de b : 8 Déplacement de ad: 0 Déplacement de bd: 4

A::m()

A::m()

Notez que les membres ad des deux classes (B l'hérite de A) sont au même déplacement. C'est important puisque l'on peut passer à la fonction f soit un pointeur vers un objet A soit un pointeur vers un objet de n'importe quel type dérivé de (i.e. qui hérite de) A. La Figure 7.20 montre le code assembleur (édité) de la fonction (généré par gcc).

Image non disponible

Notez que la sortie de la méthode m de A a été produite à la fois par l'objet a et l'objet b. D'après l'assembleur, on peut voir que l'appel à A::m() est codé en dur dans la fonction. Dans le cadre d'une vraie programmation orientée objet, la méthode appelée devrait dépendre du type d'objet passé à la fonction. On appelle cela le polymorphisme. Le C++ désactive cette fonctionnalité par défaut. On utilise le mot-clé virtual pour l'activer. La Figure 7.21 montre comment les deux classes seraient modifiées.

Image non disponible

Rien dans le restet du code n'a besoin d'être changé. Le polymorphisme peut être implémenté de beaucoup de manières. Malheureusement, l'implémentation de gcc est en transition au moment d'écrire ces lignes et devient beaucoup plus compliquée que l'implémentation initiale. Afin de simplifier cette explication, l'auteur ne couvrira que l'implémentation du polymorphisme que les compilateurs Microsoft et Borland utilisent. Cette implémentation n'a pas changé depuis des années et ne changera probablement pas dans un futur proche.

Avec ces changements, la sortie du programme change :

Sélectionnez

Size of a: 8 Déplacement de ad : 4

Size of b: 12 Déplacement de ad : 4 Déplacement de bd : 8

A::m()

B::m()

Maintenant, le second appel à f appelle la méthode B::m(), car on lui passe un objet B. Ce n'est pas le seul changement cependant. La taille d'un A vaut maintenant 8 (et 12 pour B). De plus, le déplacement de ad vaut maintenant 4, plus 0. Qu'y a-t-il au déplacement 0 ? La réponse à cette question est liée à la façon dont est implémenté le polymorphisme.

Une classe C++ qui a une (ou plusieurs) méthode(s) virtuelle(s) a un champ caché qui est un pointeur vers un tableau de pointeurs sur des méthodes(44). Cette table est souvent appelée la vtable. Pour les classes A et B ce pointeur est stocké au déplacement 0. Les compilateurs Windows placent toujours ce pointeur au début de la classe au sommet de l'arbre d'héritage.

En regardant le code assembleur (Figure 7.22) généré pour la fonction f (de la Figure 7.19) dans la version du programme avec les méthodes virtuelles, on peut voir que l'appel à la méthode m ne se fait pas via une étiquette.

Image non disponible

La ligne 9 trouve l'adresse de la vtable de l'objet. L'adresse de l'objet est placée sur la pile ligne 11. La ligne 12 appelle la méthode virtuelle en se branchant à la première adresse dans la vtable (45). Cet appel n'utilise pas d'étiquette, il se branche à l'adresse du code sur lequel pointe EDX. Ce type d'appel est un exemple de liaison tardive (late binding). La liaison tardive repousse le choix de la méthode à appeler au moment de l'exécution du code. Cela permet d'appeler la méthode correspondant à l'objet. Le cas normal (Figure 7.20) code en dur un appel à une certaine méthode et est appelé liaison précoce (early binding), car la méthode est liée au moment de la compilation.

Le lecteur attentif se demandera pourquoi les méthodes de classe de la Figure 7.21 sont explicitement déclarées pour utiliser la convention d'appel C en utilisant le mot-clé __cdecl. Par défaut, Microsoft utilise une convention différente de la convention C standard pour les méthodes de classe C++. Il passe le pointeur sur l'objet sur lequel agit la méthode via le registre ECX au lieu d'utiliser la pile. La pile est toujours utilisée pour les autres paramètres explicites de la méthode. Le modificateur __cdecl demande l'utilisation de la convention d'appel C standard. Borland C++ utilise la convention d'appel C par défaut.

Observons maintenant un exemple légèrement plus compliqué (Figure 7.23).

Image non disponible

Dans celui-là, les classes A et B ont chacune deux méthodes : m1 et m2. Souvenez-vous que comme la classe B ne définit pas sa propre méthode m2, elle hérite de la méthode de classe de A. La Figure 7.24 montre comment l'objet b apparaît en mémoire.

Image non disponible

La Figure 7.25 montre la sortie du programme.

Image non disponible

Tout d'abord, regardons l'adresse de la vtable de chaque objet. Les adresses des deux objets B sont les mêmes, ils partagent donc la même vtable. Une vtable appartient à une classe pas à un objet (comme une donnée membre static). Ensuite, regardons les adresses dans les vtables. En regardant la sortie assembleur, on peut déterminer que le pointeur sur la méthode m1 est au déplacement 0 (ou dword 0) et celui sur m2 est au déplacement 4 (dword 1). Les pointeurs sur la méthode m2 sont les mêmes pour les vtables des classes A et B, car la classe B hérite de la méthode m2 de la classe A.

Les lignes 25 à 32 montrent comment l'on pourrait appeler une fonction virtuelle en lisant son adresse depuis la vtable de l'objet (46). L'adresse de la méthode est stockée dans un pointeur de fonction de type C avec un pointeur this explicite. D'après la sortie de la Figure 7.25, on peut voir comment cela fonctionne. Cependant, s'il vous plaît, n'écrivez pas de code de ce genre ! Il n'est utilisé que pour illustrer le fait que les méthodes virtuelles utilisent la vtable.

Il y a plusieurs leçons pratiques à tirer de cela. Un fait important est qu'il faut être très attentif lorsque l'on écrit ou qu'on lit des variables de type classe depuis un fichier binaire. On ne peut pas utiliser simplement une lecture ou une écriture binaire, car cela lirait ou écrirait le pointeur vtable depuis le fichier ! C'est un pointeur sur l'endroit où la vtable réside dans la mémoire du programme et il varie d'un programme à l'autre. La même chose peut arriver avec les structures en C, mais en C, les structures n'ont des pointeurs que si le programmeur en définit explicitement. Il n'y a pas de pointeurs explicites déclarés dans les classes A ou B.

Une fois encore, il est nécessaire de réaliser que des compilateurs différents implémentent les méthodes virtuelles différemment. Sous Windows, les objets de la classe COM (Component Object Model) utilisent des vtables pour implémenter les interfaces COM (47). Seuls les compilateurs qui implémentent les vtables des méthodes virtuelles comme le fait Microsoft peuvent créer des classes COM. C'est pourquoi Borland utilise la même implémentation que Microsoft et une des raisons pour lesquelles gcc ne peut pas être utilisé pour créer des classes COM.

Le code des méthodes virtuelles est identique à celui des méthodes non virtuelles.

Seul le code d'appel est différent. Si le compilateur peut être absolument sûr de la méthode virtuelle qui sera appelée, il peut ignorer la vtable et appeler la méthode directement (p.e., en utilisant la liaison précoce).

VIII-B-6. Autres fonctionnalités C++

Le fonctionnement des autres fonctionnalités du C++ (p.e., le RunTime Type Information, la gestion des exceptions et l'héritage multiple) dépasse le cadre de ce livre. Si le lecteur veut aller plus loin, The Annotated C++ Reference Manual de Ellis et Stroustrup et The Design and Evolution of C++ de Stroustrup constituent un bon point de départ.


IX. Annexe A : Instructions 80x86

IX-A. Instructions hors virgule flottante

Cette section liste et décrit les actions et les formats des instructions hors virgule flottante de la famille de processeurs Intel 80x86.

Les formats utilisent les abréviations suivantes :

Image non disponible

Elles peuvent être combinées pour les instructions à plusieurs opérandes. Par exemple, le format R,R signifie que l'instruction prend deux opérandes de type registre. Beaucoup d'instructions à deux opérandes acceptent les mêmes opérandes. L'abréviation O2 est utilisée pour représenter ces opérandes : R,R R,M R,I M,R M,I. Si un registre 8 bits ou un octet en mémoire peuvent être utilisés comme opérande, l'abréviation R/M8 est utilisée.

Le tableau montre également comment les différents bits du registre FLAGS sont modifiés par chaque instruction. Si la colonne est vide, le bit correspondant n'est pas modifié du tout. Si le bit est toujours positionné à une valeur particulière, un 1 ou un 0 figure dans la colonne. Si le bit est positionné à une valeur qui dépend des opérandes de l'instruction, un C figure dans la colonne. Enfin, si le bit est modifié de façon indéfinie, un « ? » figure dans la colonne. Comme les seules instructions qui changent le drapeau de direction sont CLD et STD, il ne figure pas parmi les colonnes FLAGS.

Image non disponible Image non disponible Image non disponible Image non disponible Image non disponible Image non disponible

IX-B. Instruction en virgule flottante

Dans cette section, la plupart des instructions du coprocesseur mathématique du 80x86 sont décrites. La section description décrit brièvement l'opération effectuée par l'instruction. Pour économiser de la place, il n'est pas précisé si l'instruction décale la pile ou non.

La colonne format indique le type d'opérande pouvant être utilisé avec chaque instruction. Les abréviations suivantes sont utilisées :

Image non disponible

Les instructions nécessitant un Pentium Pro ou supérieur sont signalées par un astérisque (*).

Image non disponible Image non disponible

(1)

L'indice 2 est utilisé pour indiquer que le nombre est représenté en binaire, pas en
décimal.

(2)

S'il n'est pas clair que le point de départ est important, essayez de convertir l'exemple en partant de la gauche.

(3)

En fait, l'ASCII n'utilise que les 7 bits les plus faibles et donc n'a que 128 valeurs à utiliser.

(4)

Cependant, ils peuvent également utiliser une interface de plus bas niveau au niveau du noyau.

(5)

Les nombres à virgule flottante en simple précision sont équivalents aux variables float en C.

(6)

TWORD définit une zone mémoire de 10 octets. Le coprocesseur virgule flottante utilise ce type de données.

(7)

Le fichier asm_io.inc (et le fichier objet asm_io que requiert asm_io.inc) sont dans les exemples de code à télécharger sur la page web de ce tutoriel, http://www.drpaulcarter.com/pcasm.

(8)

Le Chapitre III parle de ce registre.

(9)

GNU est un projet de Free Software Foundation (http://www.fsf.org).

(10)

http://www.delorie.com/djgpp.

(11)

Il est courant de penser que les fichiers ont un caractère EOF comme dernier caractère. Ce n'est pas vrai !

(12)

La raison de cette nécessité sera exposée plus tard.

(13)

2 est le seul nombre premier pair.

(14)

Cet opérateur est différent des opérateurs && binaire et & unaire !

(15)

Application Programming Interface, Interface pour la Programmation d'Applications

(16)

Signie Portable Operating System Interface for Computer Environments, Interface Portable de Système d'Exploitation pour les Environnements Informatiques. Un standard développé par l'IEEE basé sur UNIX.

(17)

En fait, un paramètre de type mode_t qui est défini comme entier par un typedef.

(18)

En fait, passer d'une représentation à l'autre pour entier consiste à inverser l'ordre des octets ; donc, convertir de little vers big ou de big vers little est la même opération. Donc, ces deux fonctions font la même chose.

(19)

En fait, il est également possible d'empiler des mots, mais en mode protégé 32 bits, il est mieux de ne travailler qu'avec des doubles-mots sur la pile.

(20)

Il est autorisé d'ajouter une constante à un registre lors de l'utilisation de l'adressage indirect. Des expressions plus complexes sont également possibles. Ce sujet est traité dans le chapitre suivant.

(21)

GAS est l'assembleur GNU que tous les compilateurs GNU utilisent. Il utilise la syntaxe AT&T qui est très différente des syntaxes relativement similaires de MASM, TASM et NASM.

(22)

Le compiltateur C Watcom est un exemple qui n'utilise pas les conventions standard par défaut. Voyez les sources exemple pour Watcom pour plus de détails.

(23)

Un programme multithreadé a plusieurs threads d'exécution. C'est-à-dire que le programme lui-même est multitâche.

(24)

Positionner AH à zéro équivaut à supposer implicitement que AL est un nombre non signé. S'il est signé, l'action appropriée serait d'insérer une instruction CBW entre les lignes 6 et 7.

(25)

On peut indiquer une taille, mais elle n'est pas prise en compte par le compilateur.

(26)

Une autre complication est qu'on ne peut pas copier la valeur du registre DS dans ES directement en utilisant une instruction MOV. À la place, la valeur de DS doit être copiée dans un registre universel (comme AX) puis être copié depuis ce registre dans ES, ce qui utilise deux instructions MOV.

(27)

Un préfixe d'instruction n'est pas une instruction, c'est un octet spécial qui est placé avant une instruction de chaîne qui modifie son comportement. D'autres préfixes sont également utilisés pour modifier le segment par défaut des accès mémoire.

(28)

Le type long double de certains compilateurs (comme celui de Borland) utilise cette précision étendue. Par contre, d'autres compilateurs utilisent la double précision à la fois pour les double et long double (c'est autorisé par le C ANSI).

(29)

La seule différence est que pour l'infini et les valeurs indéfinies, l'exposant décalé vaut 7FF et non pas FF.

(30)

La racine d'une fonction est une valeur x telle que f(x) = 0.

(31)

Cependant, le 80486SX n'avait pas de coprocesseur intégré. Il y avait une puce 80487SX pour ces machines.

(32)

Voyez le chapitre sur la gestion de la mémoire virtuelle de n'importe quel livre sur les Systèmes d'Exploitation pour une explication de ce terme.

(33)

En fait, le standard ANSI/ISO C laisse une certaine liberté au compilateur sur la façon d'organiser les bits. Cependant, les compilateurs C courants (gcc, Microsoft et Borland) organisent les champs comme cela.

(34)

Small Computer Systems Interface, un standard de l'industrie pour les disques durs, etc.

(35)

Mélanger différents types de champs de bits conduit à un comportement très étrange ! Le lecteur est invité à tester.

(36)

La correspondance n'a pas à être exacte, le compilateur prendra en compte les correspondances trouvées en transtypant les arguments. Les règles de ce procédé sont en dehors de la portée de ce livre. Consultez un livre sur le C++ pour plus de détails.

(37)

Bien sûr, il faudrait déclarer la fonction avec une édition de liens en C, comme nous en a7vons parlé dans la Section IIX.B.1.

(38)

Les compilateurs supportent souvent cette fonctionnalité comme une extension du C ANSI.

(39)

Souvent appelés fonctions membres en C++ ou plus généralement méthodes.

(40)

Le compilateur gcc inclut son propre assembleur appelé gas. L'assembleur gas utilise la syntaxe AT&T et donc le compilateur produit un code au format gas. Il y a plusieurs sites sur le web qui expliquent les différences entre les formats INTEL et AT&T. Il existe également un programme gratuit appelé a2i (http://membres.multimania.fr/placr/a2i.html), qui passe du format AT&T au format NASM.

(41)

Comme d'habitude, rien n'est caché dans le code assembleur !

(42)

Pourquoi ? Car les opérations d'addition commenceront ainsi toujours par le début du tableau et avanceront.

(43)

Voyez le code source d'exemple pour obtenir le code complet de cet exemple. Le texte ne se réfèrera qu'à certaines parties du code.

(44)

Pour les classes sans méthode virtuelle, les compilateurs C++ rendent toujours la classe compatible avec une structure C normale qui aurait les mêmes données membres.

(45)

Bien sûr, la valeur est déjà dans le registre ECX. Elle y a été placée à la ligne 8 et la ligne 10 pourrait être supprimée et la ligne suivante changée de façon à empiler ECX. Le code n'est pas très efficace, car il a été généré en désactivant les optimisations du compilateur.

(46)

Souvenez-vous, ce code ne fonctionne qu'avec les compilateurs MS et Borland, pas gcc.

(47)

Les classes COM utilisent également la convention d'appel __stdcall, pas la convention C standard.


General Purpose Registers

There are eight 32 bit general purpose registers on the x86 processor: eax, ebx, ecx, edx, esi, edi, ebp, and esp. You will use these registers extensively - the others are used only rarely, except by debuggers and the like. These general purpose registers can be used for any purpose but many instructions work more efficiently when used with particular registers and some instructions absolutely require a specific register.

The eax register (accumulator) is where you will do most arithmetic and logical computations. You can do most arithmetic and logical operations in other registers but it is often more efficient to use the eax register.

The ebx register (base) is routinely used to hold indirect addresses.

The ecx register (count) is used particularly well as the count register much like one would use 'i' in for loops in C. One instruction in particular jecxz (jump if ecx == zero) uses this register to branch to another section of code.

The edx register (data) has special purposes too. When an addition overflows for example, the overflow is held in this register.

The esi and edi registers (source index, destination index) are used extensively when accessing memory. Strings for example, or arrays.

The ebp register (base pointer) is often used to access local variables and is used in this regard to point to a temporary stack.

The esp register (stack pointer) is crucial to a program in many ways - it points to the program stack which holds the return address from a subroutine and passed values to that subroutine. If this register become corrupted you program will crash.

Within these registers there are what I'll call sub-registers, meaning that the 32 bit registers can be used as parts rather than the whole 32 bits. Intel calls these words sized (16 bit) registers ax, bx, cx, dx, si, di, bp. Some of these in turn are split in half to give two 8 bit registers per 16 bit register.

So we get

ah and al from ax

bh and bl from bx

ch and cl from cx

dh and dl from dx

The eight, 8 bit registers are not independent, ah is the high byte of ax and al is the low byte of ax. You can deduce the other from the diagram below that shows the general purpose register set plus the segment registers.

EAX | Accumulator | most arithmetic and logical computations
EBX | Base | commonly used to hold indirect addresses
ECX | Count | use it to count off the number of iterations in a loop
EDX | Data | holds the overflow from certain arithmetic operations
ESI | Source Index | use these two registers esi & edi as pointers
EDI | Destination Index | also use with string instructions
EBP | Base Pointer | access parameters and local variables
ESI | Stack Pointer | generally not altered
CS | Code Segment | points at the segment containing the currently executing code
DS | Data Segment | points to the data segment of running process
ES | Extra Segment |
FS |  |
GS |  |
SS | Stack Segment |
EFLAGS | Condition Codes | FLAGS -- carry, parity, zero, sign, and overflow flags

EBX ESI EDI EBP and ESP should always be saved before and restored after if changed in your routine. To re-iterate, although you are at liberty to use registers for anything, certain registers are best used for specific purposes - here are two: the jump instruction jecxz looks for zero in ecx, the string instructions such as lods and stos use esi and edi as pointers to memory where loading and storing of characters will take place.


Cours : L'Assembleur

L'assembleur

Vu dans ... http://deamonftp.free.fr/deamoncrack/Pages/cours-asm.htm

Introduction

Ce cours vous semblera sans doute long et pénible (ça l'est également pour moi pour l'écrire ;-) ) car ce n'est pas forcément facile de comprendre tout ça, mais efforcez vous à bien vous concentrer dessus car c'est important si vous voulez devenir un bon cracker. Attention, il ne s'agit pas non plus d'apprendre tout cela par coeur. Je vous conseille de prendre des notes et de les garder à côté de vous quand vous crackez ou sinon, pour les faignants, d'imprimer ce cours disponible au format imprimable sur le Guide PDF.

Dans ce cours vous allez apprendre les bases de l'assembleur. L'assembleur c'est le langage de programmation que vous voyez quand vous désassemblez un fichier dans WinDasm. C'est un langage assez proche du langage machine.

Sommaire :

1° Le processeur et les registres
 A) Les registres généraux ou de travail
 B) Les registres d'offset ou pointeur
C) Les registres de segment
D) Le registre Flag
 2° La Pile et ses instructions
A) Fonctionnement de la pile
 B) L'instruction PUSH
C) L'instruction POP
 3° Les sauts conditionnels et le CMP
A) Les sauts conditionnels
B) L'instruction CMP
 4° Les opérations mathématiques
A) Addition et Soustraction : ADD et SUB
 B) Multiplication : MUL / IMUL
 C) Division : DIV / IDIV
 D) Autres division et multiplication : SHR et SHL
E) L'opposé d'un nombre : NEG
5° Les nombres à virgule et les nombres négatifs
A) Les nombres à virgule
 B) Les nombres négatifs
6 ° Les instructions logiques
A) ET logique : AND
B) OU logique inclusif : OR
 C) OU logique exclusif : XOR
 D) Non logique : NOT
 E) TEST
7° La mémoire et ses instructions
A) LEA
 B) MOVSx
 C) STOSx
 8° Fin du cours !

1° Le processeur et les registres

Les données dont le processeur a besoin sont stockées dans des "cases" qu'on appelle des registres. Il existe plusieurs types de registres et chacun a son utilité.

  • - registres généraux ou de travail
    Ils servent à manipuler des données, à transférer des paramètres lors de l’appel de fonction DOS et à stocker des résultats intermédiaires.

  • - registres d’offset ou pointeur
    Ils contiennent une valeur représentant un offset à combiner avec une adresse de segment

  • - registres de segment
    Ils sont utilisés pour stocker l’adresse de début d’un segment. Il peut s’agir de l’adresse du début des instructions du programme, du début des données ou du début de la pile.

  • - registre de flag
    Il contient des bits qui ont chacun un rôle indicateur.

A) Les registres généraux ou de travail

Les 4 registres généraux les plus utilisés sont : AX , BX , CX et DX.

  • AX -- accumulateur -- sert à effectuer des calculs arithmétiques ou à envoyer un paramètre à une interruption

  • BX -- registre auxiliaire de base -- sert à effectuer des calculs arithmétiques ou bien des calculs sur les adresses

  • CX -- registre auxiliaire (compteur) -- sert généralement comme compteur dans des boucles

  • DX -- registre auxiliaire de données -- sert à stocker des données destinées à des fonctions

Ceci est leur utilisation théorique, mais dans la pratique ils peuvent être utilisés à d'autres usages.

Ces registres sont de 16 bits (petit rappel : 8 bits = 1 octet et 1 bit = 0 ou 1) et chacun de ses registres est divisé en 2 parties : L (comme Low pour désigner les bits de poids faible) et H (pour High afin de désigner les bits de poids forts). Par exemple : AX (AL et AH), BX (BL et BH), CX (CL et CH), DX (DL et DH).
Pour obtenir un registre de 32 bits il faut rajouter un "E" (pour Extended, en français "étendu") devant le registre. Par exemple EAX (32 bits) pour AX (16 bits), (EBX pour BX, ECX pour CX, EDX pour DX). Il existe également des EAH ou EAL, mais la partie haute d'un registre 32 bits ne sera pas directement accessible.

Les bits correspondent en fait à leur capacité : 8 bits = nombre entre 0 et 255 (en décimal) au maximum, 16 bits = 65535 au maximum, 32 bits = 4 294 967 295 au maximum.

EAX (32 bits)
 | AX (16 bits)
 | AH (8 bits) | AL (8 bits)

B) Les registres d'offset ou pointeur

Voici les différents registres :

  • IP -- Pointeur d'instruction -- est associé au registre de segment CS (CS :IP) pour indiquer la prochaine instruction à exécuter. Ce registre ne pourra jamais être modifié directement ; il sera modifié indirectement par les instructions de saut, par les sous-programmes et par les interruptions

  • SI -- Index de source -- est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est associé au registre de segment DS.

  • DI -- Index de destination -- est principalement utilisé lors d’opérations sur des chaînes de caractères ; il est normalement associé au registre de segment DS ; dans le cas de manipulation de chaînes de caractères, il sera associé à ES
  • SP -- Pointeur de pile -- est associé au registre de segment SS (SS :SP) pour indiquer le dernier élément de la pile.
  • BP -- Pointeur de base -- est associé au registre de segment SS (SS :BP) pour accéder aux données de la pile lors d’appels de sous-programmes (CALL).

Comme les registres généraux, ces registres peuvent être étendues à 32 bits en ajoutant le "E" : (EIP, ESI, EDI, ESP, EBP). Par contre ils ne possèdent pas de partie 8 bits (il n' y aura pas de EIH ou ESL par exemple).

Les registres SI et DI sont le plus souvent employés pour les instructions de chaîne et pour accéder à des blocs en mémoire.

Ces registres (sauf IP) peuvent apparaître comme des opérandes dans toutes les opérations arithmétiques et logiques sur 16 bits

ESI (32 bits)

SI (16 bits)

C) Les registres de segment

Le processeur utilise des adresses pour stocker ou récupérer des données. L'adresse est composée de 2 parties de 32 bits (ou 16 bits selon le mode utilisé). La première est le segment et la deuxième est l'offset. L'adresse est présenté comme cela : segment:offset (par exemple : 0A000h:00000h). Voici les différents segments :

  • CS -- Registre segment de code -- indique l’adresse du début des instructions d’un programme ou d’une sous-routine

  • DS -- Registre segment de données -- contient l’adresse du début des données de vos programmes. Si votre programme utilise plusieurs segments de données, cette valeur devra être modifié durant son exécution

  • SS -- Registre segment de pile -- pointe sur une zone appelée la pile

  • ES -- Registre segment supplémentaire (Extra segment) -- est utilisé, par défaut, par certaines instructions de copie de bloc. En dehors de ces instructions, le programmeur est libre de l’utiliser comme il l’entend

  • FS -- Registre segment supplémentaire -- Rôle semblable à ES

  • GS -- Registre segment supplémentaire -- Rôle semblable à ES

D) Le registre Flag

Ce registre est un ensemble de 16 bits. Mais cet ensemble ne signifie rien, car les bits sont modifiés un par un. Et la modification d'un seul bit peut changer le comportement du programme. Les bits de cet ensemble sont appelés "indicateurs".

Bit 1 : CF
Bit 2 : 1
Bit 3 : PF
Bit 4 : 0
Bit 5 : AF
Bit 6 : 0
Bit 7 : ZF
Bit 8 : SF
Bit 9 : TF
Bit 10 : IF
Bit 11 : DF
Bit 12 : OF
Bit 13 : IOPL
Bit 14 : NT
Bit 15 : 0
Bit 16 : RF
Bit 17 : VM

Les bits 1, 5, 12, 13, 14, 15 de ce registre Flag de 16 bits ne sont pas utilisés.

Les instructions arithmétiques, logiques et de comparaison modifient la valeur des indicateurs.
Les instructions conditionnelles testent la valeur des indicateurs et agissent en fonction du résultat.

Voici à quoi les principaux indicateurs servent :

CF : Carry Flag - Indicateur de retenue

A la suite d'une opération, si le résultat est codé sur un nombre supérieur de bits, le bit de trop sera mis dans CF. Plusieurs instructions peuvent modifier son état : CLC pour le mettre à 0, STC à 1 et CMC inverse sa valeur.

PF : Parity Flag - Indicateur de parité

Egal à 1 si le nombre de bits d'une opérande (paramètre d'une instruction) est pair.

AF : Auxiliary carry Flag - Indicateur de retenue auxiliaire

Cet indicateur ressemble à CF.

ZF : Zero Flag - Indicateur zéro

Si le résultat d'une opération est nul (égal à 0) ZF passera à 1. Cet indicateur est souvent utilisé pour savoir si deux valeurs sont égales (en les soustrayant).

SF : Sign Flag - Indicateur de signe

SF passe à 1 quand le résultat est signé (négatif ou positif).

IF : Interruption Flag - Indicateur d'interruption

Il permet d'enlever la possibilité au processeur de contrôler les interruptions. Si IF = 1 les interruptions sont prises en compte et si IF = 0 elles sont ignorées. L'instruction STI met IF à 1 et CLI le met à 0.

DF : Direction Flag - Indicateur de direction

C'est ce Flag qui donne l'indication sur la manière de déplacer les pointeurs (références) lors des instructions de chaînes (soit positivement, soit négativement). STD le met à 1 et CLD à 0.

OF : Overflow Flag - Indicateur de dépassement

Il permet de trouver et de corriger certaines erreurs produites par des instructions mathématiques. Très utile pour éviter les plantages. Si OF=1 alors nous avons affaire à un Overflow.

Les autres indicateurs ne sont pas importants nous n'allons donc pas nous attarder dessus.

Petit exemple sur ces indicateurs avec une opération en binaire :

0 1 1 0
+ 0 1 0 1
--------
1 0 1 1
  • ZF=0 (le résultat 1011 n'est pas nul)
  • SF=1 (le signe du résultat est négatif)
  • CF=0 (il n'y a pas de retenue)

2° La Pile et ses instructions

A) Fonctionnement de la pile

La pile ("stack" en anglais) est un emplacement qui sert à stocker de petites données. Cette mémoire temporaire fonctionne comme une pile d'assiette : c'est-à-dire que la dernière assiette que vous empilez sera la première à être prise et la première assiette empilée (celle qui est tout en dessous) sera la dernière prise. J'espère avoir été clair dans cette métaphore. Pour prendre un vrai exemple, si je mets la valeur "A" dans la pile puis je mets "B" et après "C". Pour récupérer ces valeurs le processeur va d'abord prendre "C", puis "B" et ensuite "A". Là si vous ne comprenez toujours pas je ne peux plus rien pour vous !

Pour l'API GetDlgItemText, Windows a besoin de ces informations :

HWND hDlg, // (1) handle de la boîte de dialogue
int nIDDlgItem, // (2) identifiant de la boîte de dialogue
LPTSTR lpString, // (3) pointe vers le buffer pour le texte
int nMaxCount // (4) taille maximum de la chaîne

On peut donc mettre avant d'appeler cette fonction :

MOV EDI, [ESP+00000220] place l'handle de la boite de dialogue dans EDI
PUSH 00000100 push (4) taille maxi de la chaîne
PUSH 00406130 push (3) adresse du buffer pour le texte
PUSH 00000405 push (2) identificateur du contrôle
PUSH EDI push (1) hanlde de la boite de dialogue
CALL GetDlgItemText appelle la fonction

B) L'instruction PUSH

Vous avez pu voir cette instruction au dessus. Push signifie "pousser" en français. C'est avec cette instruction qu'on place une valeur sur la pile. La valeur "poussé" doit être de 16 ou 32 bits (souvent un registre) ou une valeur immédiate.

Par exemple :
PUSH AX
PUSH BX
PUSH 560
AX est placé en haut de la pile en premier mais ce sera 560 qui en ressortira en premier.

C) L'instruction POP

Mettre des données sur la pile c'est bien, mais le but c'est de les récupérer par la suite. C'est donc à ça que sert l'instruction POP : récupérer les données placées sur la pile.

Par exemple (suite à l'exemple au dessus) :
POP CX
POP BX
POP AX
Ces instructions mettront la première instruction du haut de la pile dans CX, c'est à dire 560. Et ensuite BX reviendra dans BX et AX dans AX.

La pile est très utilisée pour retrouver les valeurs intactes des registres. Car le nombre de registres est limité, donc on utilise la pile lorsque l'on n'a plus de place. Mais la pile est plus lente que les registres.

3° L es sauts conditionnels et le CMP

A) Les sauts conditionnels

Dans les différents sauts il y a les inconditionnels (qui sautent sans conditions) comme le JMP (Jump) et ceux qui sont conditionnels (qui sautent donc si la condition est vérifiée). Dans cette dernière "famille" il y a du monde. ;-) Les valeurs des indicateurs sont nécessaires pour la vérification de la condition.

Voici les principaux sauts, sachez que J = Jump if (Saut si) :

Indicateur | Valeur | Saut | Signification
CF | 1 | JB | Below - Inférieur
 |  | JBE | Below or Equal - Inférieur ou égal
 |  | JC | Carry - Retenue
 |  | JNAE | Not Above or Equal - Non supérieur ou égale
 | 0 | JA | Above - Supérieur
 |  | JAE | Above or Equal - Supérieur ou égal
 |  | JNB | Not Below - Non inférieur
 |  | JNC | Not Carry - Pas de retenue
ZF | 1 | JE | Equal - Egal
 |  | JNA | Not Above - Non supérieur
 |  | JZ | Zero - Zero (Saut en cas d'égalité)
 | 0 | JNBE | Not Below or Equal - Non supérieur ou égal
 |  | JNE | Not Equal - Non égal
 |  | JNZ | Not Zero - Non zéro (saut si différent)
PF | 1 | JP | Parity - Pair
 |  | JPE | Parity Even - Pair
 | 0 | JNP | Not Parity - Non pair
 |  | JPO | Parity Odd - Non pair
OF | 1 | JO | Overflow - Dépassement
 | 0 | JNO | Not Overflow - Pas de dépassement
SF | 1 | JS | Signed - Signé
 | 0 | JNS | Not Signed - Non signé

Et quelques autres :

JG - JNLE
Greater (arithmétiquement supérieur) - Not Less or Equal (arithmétiquement non inférieur ou égale)
ZF=0 et SF=OF

JGE - JNL
Greater or Equal (arithmétiquement supérieur ou égal) - Not Less (non inférieur arithmétiquement)
SF=OF

JL - JNGE
Less (arithmétiquement inférieur) - Not Greater or Equal (arithmétiquement non supérieur ou égal)
SF (signé)=OF

JLE - JNG
Less or Equal (arithmétiquement inférieur ou égal) - Not Greater (non supérieur arithmétiquement)
ZF=1 ou SF (signé)=OF

B) L'instruction CMP

Cette instruction permet d'effectuer des tests entre des valeurs et de modifier les flags suivant le résultat. Le fonctionnement du CMP est semblable au SUB (que l'on verra plus loin et qui sert pour la soustraction). Il s'utilise de cette façon : CMP AX, BX. Cela permet de vérifier si ces 2 valeurs sont égales car il fait AX-BX, si c'est égal à 0 alors ZF=1 sinon ZF=0.

Le plus souvent il est suivi de ces sauts et leur inverse (en rajoutant un "N" après le "J", par exemple JNE pour JE...) :

JA : plus grand que (nombres non-signés)
JB : plus petit que (nombres non-signés)
JG : plus grand que (nombres signés)
JL : plus petit que (nombres signés)
JE ou JZ : égal à (signé et non-signé)

4° L es opérations mathématiques

A) Addition et Soustraction : ADD et SUB

Ces 2 instructions nécessitent 2 opérandes : la source et la destination. Ce qui donnera : Destination = Source + Destination. Et en assembleur : ADD Destination, Source.
Exemple avec AX = 4 et BX = 6 :

ADD AX, BX

Après cette opération : AX = 4+6 = 10 et BX = 6

Le fonctionnement de SUB est identique.

Il est impossible d'ajouter ou soustraire un 16 bits avec un 8 bits par exemple. Donc ADD BX, CL sera impossible !

B) Multiplication : MUL / IMUL

L'instruction MUL est utilisé pour les nombres non signés et IMUL pour les signés (petit rappel : les nombres signés peuvent être négatifs contrairement aux non signés, par exemple en 16 bits en non signé les nombres vont de 0 à 65535 et en signé ils vont de -32768 à 32767). Contrairement aux opérations précédentes, la multiplication n'a besoin que d'une seule opérande : la source. La destination et donc le nombre qui devra être multiplié se trouvera toujours et obligatoirement dans AX. La source est un registre.

Exemple avec AX = 4 et BX = 6

MUL BX

Résultats : AX = 4*6 = 24 et BX = 6

IMUL fonctionne de la même façon, sauf qu'il faut utiliser l'instruction CWD (convert word to doubleword), qui va permettre "d'étendre" le signe de AX dans DX. Si cela n'est pas fait les résultats pourront être erronés.

C) Division : DIV / IDIV

Pareil que MUL et IMUL : DIV sert pour les nombres non signés et IDIV pour les non signés. Cette instruction divise la valeur de AX par la source.

Exemple avec AX = 18 et BX = 5 :

IDIV BX

Résultats : AX = 3 et DX = 3 (DX étant le reste)

D) Autre division et multiplication : SHR et SHL

Ces instructions permettent de diviser des registres avec SHR et les multiplier avec SHL. Ils demandent 2 opérandes : le nombre à diviser et le diviseur pour SHR et le nombre à multiplier et le multiplicateur pour SHL. Ils fonctionnent en puissance de 2. Donc si on veut diviser AX par 4 on mettra : SHR AX, 2 (car 2^2 = 4). Pour diviser BX par 16 on mettra : SHR BX,4 (car 2^4 = 16). Pour SHL c'est pareil : pour multiplier CX par 256 on mettra donc : SHL CX,8 (2^8 = 256). Pour multiplier ou diviser des nombres qui ne sont pas des puissances de 2 il faudra combiner des SHR ou SHL.
Petit rappel de mathématiques : x^0 donnera toujours 1 quelque soit x !

Exemple : on veut multiplier 5 par 384. On voit que 384 = 256 + 128, ce qui nous arrange car 256 et 128 sont des puissances de 2.

  • MOV AX,5 >> On met 5 dans AX >> AX = 5

  • MOV BX,AX >> On met AX dans BX >> BX = AX

  • SHL AX,8 >> On multiplie AX par 256 (2^8) >> AX = AX*256 = 5*256 = 1280

  • SHL BX,7 >> On multiplie BX par 128 (2^7) >> BX = BX*128 = 5*128 = 640

  • ADD AX,BX >> On ajoute BX à AX >> AX = AX + BX = 1280 + 640 = 1920

Et vous pouvez vérifier : 5*384 = 1920 ! ;-)

Mais vous me direz : "pourquoi ne pas faire simplement un MUL ?". Et bien car SHL est beaucoup plus rapide à exécuter. Mais pour des nombres qui ont une décomposition trop longue, mieux vaut bien sur utiliser un MUL.

E) L'opposé d'un nombre : NEG

L'instruction la plus simple à mon avis : NEG sert à rendre un nombre positif en négatif et inversement. Il ne demande qu'une seule opérande : la destination. Si AX = 5, alors un NEG AX mettra AX = -5. Ou si BX = -14 alors après le NEG BX, BX = 14.

5° L es nombres à virgule et les nombres négatifs

A) Les nombres à virgule

Le problème en assembleur c'est qu'on ne peut pas utiliser directement les nombres à virgule. Nous utiliserons les nombres à virgule fixe. Il s'agit simplement d'effectuer des calculs avec des valeurs assez grandes qui seront ensuite, redivisées pour retrouver un résultat dans un certain intervalle.

Par exemple nous voulons multiplier 20 par 0,25. Comme on ne peut pas mettre 0,25 dans un registre on va le mettre en nombre entier en le multipliant par 256 par exemple. 0.25*256 = 64. On le multipliera à 20 et ensuite ce résultat étant 256 fois trop grand on le redivisera par 256 :

  • MOV AX,64 >> On place 0,25*256 dans AX >> AX = 64

  • MOV BX,20 >> On place 20 dans BX >> BX = 20

  • MUL BX >> On multiplie AX par BX >> AX = AX*BX = 64*20 = 1280

  • SHR AX,8 >> On divise AX par 256 (2^8) >> AX = AX/256 = 1280/256 = 5

Le résultat de 20*0,25 est donc bien égal à 5.

Faîtes bien attention à ne pas effectuer de dépassement, surtout avec les nombres signés ou les grands nombres. En utilisant SHR ou SHL, plus l'opérande est grande et plus le résultat sera précis, car il y aura plus de décimales disponibles.

B) Les nombres négatifs

Un autre problème, c'est qu'en décimal pour un nombre négatif on place un "-" devant le nombre, mais en binaire on ne peut mettre que des 0 ou des 1. Pour pouvoir mettre un nombre en négatif on utilise la méthode du complément à 2. Voici en quoi cela consiste :

  • Convertir le nombre en binaire s'il ne l'est pas déjà

  • Inverser les bits du nombre (inverser les 0 en 1 et 1 en 0)

  • Ajouter 1 au nombre obtenu

Voici un exemple avec le nombre décimal 5 au format 8 bits (octet) :

  • On le convertit en binaire : 5(d) = 00000101(b)

  • On inverse : 00000101 --> 11111010 (ici le format, 8 bits, est important car en 4 bits par exemple cela donnera : 1010)

  • On ajoute 1 : 11111010 + 00000001 = 11111011

-5 est donc égal à 11111011 en binaire.

Pour que le nombre soit négatif il faut qu'il soit signé. En 8 bits en non signé les nombres vont de 0 à 255 et en signé ils vont de -128 à 127.

Quand le nombre est signé et en 8 bits, on peut savoir s'il est négatif en regardant le 8ème bit (en partant bien sur de la droite). S'il est égal à 1 alors le nombre est négatif. Pour les nombres à 4 bits c'est le 4ème qui compte.

Pour transformer -5 en hexadécimal, on décompose le nombre binaire en demi-octet (11111011 en 1111 et 1011). 1111b = 15d = Fh et 1011b = 11d = Ah. Donc -5d = FAh. (Petit rappel : d = décimal, b = binaire, h = hexadécimal, pour ceux qui n'auraient pas encore compris).

6° L es instructions logiques

Les opérations de ces instructions se font bit à bit. Et les opérandes et emplacements mémoires qu'elles utilisent sont en 8 ou 16 bits.

Petit rappel : pour convertir de tête un nombre binaire en décimal : chaque rang d'un nombre binaire est un multiple de 2.

2^7 | 2^6 | 2^5 | 2^4 | 2^3 | 2^2 | 2^1 | 2^0
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1

Ainsi par exemple 10110110 donnera 128+32+16+4+2 = 182 :

1 | 0 | 1 | 1 | 0 | 1 | 1 | 0
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1

A) Et logique : AND

AND (ET) effectue un ET LOGIQUE sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le ET LOGIQUE donne le résultat 1 uniquement quand les 2 opérandes sont 1, sinon il met 0.

opérande 1 | opérande 2 | AND
0 | 0 | 0
0 | 1 | 0
1 | 0 | 0
1 | 1 | 1

Exemple avec AX = 15 et BX = 26 :

AND AX,BX

Résultat :

0000 1111 (AX=15)
AND 0001 1010 (BX=26)
-------------
0000 1010 (AX = 10)

B) Ou logique inclusif : OR

OR effectue un OU LOGIQUE INCLUSIF sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le OU LOGIQUE INCLUSIF donne le résultat 0 uniquement quand les 2 opérandes sont 0, sinon il met 1.

opérande 1 | opérande 2 | OR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 1

Exemple avec AX = 15 et BX = 26 :

OR AX,BX

Résultat :

0000 1111 (AX=15)
OR 0001 1010 (BX=26)
-------------
0001 1111 (AX = 31)

C) Ou logique exclusif : XOR

XOR effectue un OU LOGIQUE EXCLUSIF sur 2 opérandes et le résultat se retrouvera dans la première opérande. Le OU LOGIQUE EXCLUSIF donne le résultat 1 quand les 2 opérandes sont différentes, sinon il met 0.

opérande 1 | opérande 2 | XOR
0 | 0 | 0
0 | 1 | 1
1 | 0 | 1
1 | 1 | 0

Exemple avec AX = 15 et BX = 26 :

XOR AX,BX

Résultat :

0000 1111 (AX=15)
XOR 0001 1010 (BX=26)
-------------
0001 0101 (AX = 21)

D) Non logique : NOT

NOT effectue un NON LOGIQUE sur une opérande et le résultat se retrouvera dans l'opérande. Le NON LOGIQUE inverse la valeur de chaque bit.

opérande | NOT
0 | 1
1 | 0

Exemple avec AX = 15 :

NOT AX

Résultat :

NOT 0000 1111 (AX=15)
-------------
1111 0000 (AX = -16 en signé et 240 en non signé)

E) TEST

Cette instruction teste la valeur d’un ou plusieurs bits en effectuant un ET LOGIQUE sur les opérandes. Le résultat ne modifie pas les opérandes mais les indicateurs.

Exemple :

TEST AX,1 >> Effectue un ET LOGIQUE sur le premier bit de AX avec 1, si il est égal à 1, ZF passera à 1 et inversement (si 0 alors ZF=0).

Un TEST AX,AX (souvent utilisé) permet de voir si AX est égal à 0 ou non.

7° La mémoire et ses instructions

A) LEA

Pour placer par exemple la valeur de l'offset de MESSAGE dans SI il suffit de mettre : MOV SI,Offset-Message. Mais on ne peut pas procéder de cette façon pour les registres DS, ES, FS, GS car ceux ci n'acceptent pas les valeurs numériques, ils n'acceptent que les registres. Il faudra donc passer par : MOV AX,Seg-Message puis MOV DS,AX.

Il existe une autre instruction, LEA, qui permet de placer l'offset dans un registre mais en plus court. Par exemple : LEA SI,MESSAGE placera dans SI l'offset de Message. L'utilisation de LEA à la place de MOV rend le code plus clair et plus compact.

B) MOVSx

Pour déplacer des blocs entiers de mémoires il faut utiliser les instructions : MOVSB, MOVSW ou MOVSD selon le nombre de bits à déplacer.

  • MOVSB : (B) = Byte (Octet : 8 bits) (Attention !! En anglais "byte"= un octet et "bit"= un bit)
  • MOVSW : (W) = Word (Mot : 16 bits)
  • MOVSD : (D) = DWord (Double Mot : 32 bits)

Si on veut déplacer 1000 octets (bytes) en utilisant la commande MOVSB il faudra qu'elle se répète 1000 fois. Donc l'instruction REP est utilisée comme une boucle. Il faut placer auparavant le nombre de boucles à effectuer dans le registre de compteur (CX) :

MOV CX,1000 >> Nombre de boucles à effectuer dans le compteur CX
REP MOVSB >> Déplace un octet

Pour aller plus vite on peut les déplacer par Mot (Word) :

MOV CX,500 >> 1000 bytes = 500 words
REP MOVSW >> Déplace un mot

Ou alors en Double Mot (DWord) :

MOV CX,250 >> 1000 bytes = 500 words = 250 dwords
REP MOVSD >> Déplace un Double Mot

A chaque MOVSx, DI augmente de 1,2 ou 4 bytes

C) STOSx

Pour garder des données ou les placer dans un emplacement de la mémoire on utilise les instructions STOSB, STOSW ou STOSD.

Pareil que les MOVSx, ces instructions servent respectivement à stocker un byte, un word et un dword. Par contre ils utilisent AL, AX (EAX 32bits) comme valeur à stocker. La valeur de AL,AX,EAX sera stockée dans ES:DI. A chaque STOSx, DI est augmenté de 1,2 ou 4 bytes.

Exemple :

MOV CX,10 >> nombre de répétitions placées dans CX, ici 10
MOV AX,0 >> valeur à déplacer, ici 0
REP STOSB >> stocker AX dans ES:DI

Nous aurons donc 10 bytes de 0 dans ES:DI.

8° Fin du cours !

Voilà c'est la fin du cours ! "Ouf" diront certains, "Oh quel dommage" diront les hypocrites ! ;-) Je félicite ceux qui ont réussi à lire tout ça ! ;-) J'espère que cela vous servira à mieux comprendre en crackant. Avec ça vous devriez vous sentir plus à l'aise (enfin si vous avez compris mon cours).

J'espère avoir été le plus compréhensible possible. Mais si vous ne comprenez pas quelque chose à propos de ce cours demandez sur le forum plutôt que de m'envoyer un mail sur deamon.crack (at) netcourrier.com car j'essaye de toute façon de lire tous les posts du forum et d'autres personnes que moi pourront vous aider.

Je voulais féliciter le très bon cours de Falcon sur l'assembleur qui m'a permis de découvrir l'assembleur. D'ailleurs j'ai repris un peu son plan, car je n'ai pas réussi à en faire un bien, et j'ai dû copier seulement 2-3 phrases. Mais c'est bien moi qui ai écrit tout ça, en 6-8 heures (et oui c'est très long à écrire tout ça) !

Pendant que je suis là ;-) , je remercie également tout ceux qui viennent sur mon site, ceux qui m'envoient des mails de sympathie et ceux qui participent plus ou moins activement sur le forum.

Deamon, le 15-16 juillet 2003


Accueil

Accueil CHM

Normal, dit comme ça,
vous ne risquez pas de comprendre.


Les bases indispensables pour débuter

TUTORIAL ASSEMBLEUR - chapitre 1 - Les bases indispensables pour débuter
Définition de l'assembleur

L'assembleur est un language de programmation transformant un fichier texte
  contenant des instructions, en un programme que le processeur peut
  comprendre (programme en language machine).
Ce language machine a la particularité d'ętre difficile ŕ programmer car
  il n'est composé que de nombres en héxadécimal (base 16).
  L'assembleur est une "surcouche" du language machine, il permet d'utiliser
  des instructions qui seront transformées en language machine donc il
  présente une facilité de programmation bien plus grande que le language
  machine.Le fichier texte qui contient ces instructions s'appelle le source.

Le language héxadécimal

Nous allons aborder un aspect trčs important de la programmation en
  assembleur : le systčme de numérotation en héxadécimal. Ce systčme est
  basé sur l'utilisation des chiffres et de certaines lettres de l'alphabet
  (de A ŕ F). Vous connaissez bien entendu le systčme décimal (base 10).
En assembleur, les nombres décimals sont suivis d'un "d" (1000=1000d) mais
  en principe la majorité des assembleurs calculent en décimal par défaut.
  Maintenant, prenons un systčme ŕ base 5  (les 5 doigs de votre main).
  Nous allons compter en base 5. On commence par 0 (doigt 0),1 (doigt 1),
  2 (doigt 2), 3 (doigt 3), 4 (doigt 4).
  Problčme..Comment faire pour représenter 5 ?
  C'est simple. Comme la base 5 nous permet de représenter des nombres avec
  les chiffres 0-1-2-3-4, on fait comme en base dix, on rajoute un 1 devant.
  Ce qui donne (doigt 5)=10 et (doigt 7)=12. En fait, quand on arrive ŕ la
  fin d'un "cycle", on rajoute ou augmente le premier chiffre du nombre.
  Mais revenons ŕ notre base 16. Nous allons faire la męme chose que pour
  la base 5, cependant nous ne sommes plus limités dans la représentation
  (chiffre de 0 ŕ 9). Nous arrivons ŕ 9 (9d),maintenant comment faire le 10.
  Nous allons utiliser des lettres (10d=0A) et nous irons jusqu'ŕ 15 (15d=0F).
  Nous ne pouvons pas utiliser le G et les lettres qui suivent, donc nous
  augmenterons le premier chiffre ce qui donne 16d=10.
  Continuez ainsi jusqu'ŕ 255 qui fait FF. Et aprčs. Et bien on continue.
  256d=0100h (le h et le zéro qui précčdre indiquent que ce nombre est en
  hexadécimal). 257d=101h. 65535=FFFFh. Bon, je pense que vous avez compris.
  Pour convertir des nombres du décimal en héxa, vous pouvez utiliser la
  calculatrice de Merdows en mode scientifique.
Je vais finir mon explication avec le calcul binaire. Vous savez probablement
  que les ordinateurs calculent en base 2 (0 ou 1).Le calcul binaire consiste
  ŕ mettre un 1 ou un 0 selon la valeur désirée.
  Chaque 0 ou 1 est appelé un bit, 8 bits forment un octet.

Pour le nombre 1, nous avons 00000001b (le b signifie binaire).
  Un nombre binaire se décompose comme-ceci :

          128|64|32|16|8|4|2|1

Pour le nombre 1, la premičre case est remplie (1=rempli). Pour avoir le
  nombre 2,nous aurons 00000010b et pour le nombre 3, nous aurons 00000011
  (1 case=1 - 2čme case=1 => 1+2=3).
  Le nombre 11111111b donnera 255 puisque en additionant 1+2+4+...+128,
  on obtient 255. Plus il y a de bits, plus le nombre peut ętre grand, nous
  verrons plus tard qu'il existe différentes unités selon le nombre de bits.

Le processeur et ses registres

Le processeur est composé de différentes parties. Les registres sont les
  éléments les plus importants du processeur pour celui qui prgramme en asm.
  Les registres sont souvent représentés comme des cases dans lesquelles
  sont stockés différentes valeurs de différentes tailles selon le type et
  le nom du registre. Il existe plusieurs types de registres :

        - registres généraux
        - registres d'états
        - registres de segment

Nous allons nous intéresser aux registres généraux et aux reg de segment.
On a plusieurs registres généraux qui commencent par A,B,C et D.
Ces quatres registres sont les plus utilisés.
  Le 1er,  AX (registre de 16 bits) qui se divise en deux petits registres
  de 8 bits, AL (l=low=bas) et AH (h=high=haut).
Nous avons ensuite BX (BL et BH), CX (CL et CH) et DX (DL et DH), ces
  registres sont divisés comme AX en 2 parties hautes et basses.
On peut rajouter un "E" devant les registres 16 bits afin d'obtenir un
  registre 32 bits.
Ce qui donne EAX,EBX,ECX et EDX. Notez que l'on ne peut avoir de EAH ou ECL.
  La partie haute d'un registre 32 bits, n'est pas directement accessible,
  on doit utiliser différentes instructions afin de la faire "descendre"
  dans un registre de 16 bits et pouvoir finalement l'utiliser.

Ces registres peuvent contenir une valeur correspondant ŕ leur capacité.
  AX=65535 au maximum (16 bits) et AL=255 au maximum (8 bits).
  Je répčte que la partie haute du registre 32 bits ne peut pas ętre
  modifiée comme un registre. Elle peut ętre modifiée seulement si l'on
  modifie tout le registre 32 bits (y compris la partie basse), ou par
  le biais de quelques instructions qui permettent de copier la partie
  basse du registre dans la partie haute, mais cela ne nous concerne pas
  pour l'instant.

  Les processeurs 286 et moins ne peuvent pas utiliser les registres 32 bits
  (EAX,EBX,ECX,EDX = impossible).
  Ce n'est qu'ŕ partir du 386 que l'utilisation du 32 bits est possible.
  Pour terminer, je vais parler des registres pointeurs qui sont DI
  (destination index), SI (source index), BP (base pointer), IP (instruction
  pointer) et SP (stack pointer).
  Ces registres peuvent eux aussi ętre étendus ŕ 32 bits en rajoutant un "E"
  (EDI,ESI,EIP,ESP,EBP). Ces registres n'ont pas de partie 8 bits, il n'y a
  pas de EDL ou de ESH.
  Les registres SI et DI sont le plus souvent employés pour les instructions
  de chaîne et pour acceder ŕ des blocs en mémoire.
  Le registre SP est utilisé pour acceder ŕ la pile.
  Nous ne l'employons presque pas, sauf pour du code trčs spécifique.

Le processeur et ses registres de segment

  Pour pouvoir chercher et placer des choses dans sa mémoire, un ordinateur
  a besoin de ce qu'on appelle une adresse. Celle-ci se compose de deux
  parties de 32 bits (ou 16 bits, cela dépend du mode employé).
  La premičre est le segment et la deuxičme, l'offset.
  Voici un exemple :

          0A000h:00000h (adresse de la mémoire vidéo)

  Dans cet exemple, il s'agit d'un adressage 16 bits.
  L'adressage 16 bits s'utilise en mode _réel_.
  Le mode réel est un état du processeur oů il ne peut accéder ŕ des blocs
  de taille supérieure ŕ 16 bits (65536 bytes). Le 286 introduira un mode
  dit protégé qui permet de franchir cette barričre pour pouvoir accéder ŕ
  quasiment tout la RAM en un bloc (ce mode protégé sera grandement amélioré
  ŕ partir du 386). Nous nous contenterons du mode réel au début, car il
  est plus simple mais il présente  tout de męme des limites.
  Sur 386, les segments ont été conservés pour garder une certaine comp-
  tabiltié avec les vieilles générations de PC.

Les registres de segment ne sont que lus et écris sur 16 bits.
  Le 386 utilise un offset de 32 bits (sauf en mode réel oů il travaille
  toujours en 16 bits).Voici la liste de ces segments,
  ils sont au nombre de 6 :

        - CS (code segment)
        - SS (stack segment)
        - DS (data segment)
        - ES (extra segment)
        - FS (extra segment - seulement ŕ partir du 386)
        - GS (extra segment - seulement ŕ partir du 386)

### Chapitre 1 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Les bases indispensables pour débuter

TUTORIAL ASSEMBLEUR - chapitre 1B - Les bases indispensables pour débuter

Quand faut-il utiliser l'asm ?

A mes debuts en tant que codeur, j'avais un esprit "100% asm".
Je me suis rendu compte, aprés avoir débuté l'apprentissage du C,
que programmer toutes les routines en asm ne pouvait mener qu'ŕ une
perte de temps sensible. Il est important de savoir oů et quand il
faut utiliser l'asm afin d'avoir un programme le plus performant
tout en conservant des temps de developpement les plus courts possibles.

Il est conseillé d'utiliser l'asm lors :

        - routine d'affichage de polygones/triangles (en 3D)
        - routine de Vesa/copies ecran
        - routines "systéme" (contrôle périphériques, mémoire, handlers..)
        - lors de la programmation d'une intro 4K (c'est presque évident) :)
        - dés qu'un aspect "la taille du programme doit rester petite" entre en jeu

Il est déconseillé de faire du 100% asm :

        - pour des routines qui ne demandent pas forcément ŕ étre traitées
          en "realtime" (code de précalculation, traitement de fichiers..)
        - pour coder toute une demo en asm ou des projets d'une certaine ampleur
        - dés que la taille du programme ne représente pas de probléme
        - si on veut faire un programme portable (qu'on peut utiliser dans différents
          OS), il est męme conseille d'utiliser l'asm avec parcimonie.
        - si l'on sait que l'on va souvent effectuer des changements dans ses
          routines (pour "tester")

ASM against C

Mais attention. La montée en puissance des compilateurs C a diminué
la différence de vitesse entre le code asm fait par le programmeur
et le męme code en C que le compilateur va optimiser.

Des compilateurs C tels que Watcom (commercial) et Djgpp (gratuit) sont
désormais capables de surpasser les męmes routines écrites en asm.
Cela s'explique par le fait que les compilateurs prennent en considération
les caractéristiques des processeurs et optimisent de maniére trés poussée
en cherchant les meilleurs combinaisons pour avoir un gain de vitesse
maximum. J'ai entendu dire que Djgpp effectue des "passes", il compile
le programme une 1ere fois, ensuite, il reéxamine le code en optimisant
ŕ chaque fois un peu plus pour finalement aboutir ŕ un code extrémement
bien ficellé. Watcom travaille probablement de la męme facon. D'aprés
mes propres expériences, les compilateurs C sont trés ŕ l'aise sur
les programmes complexes oů interviennent de nombreuses boucles.

C'est pourquoi, je conseille ŕ tous ceux qui veulent se lancer dans
la programmation d'apprendre ŕ la fois le C et l'asm et d'utiliser
les 2 dans leur programme. Les compilateurs C sont moins performants
sur le plan des optimisations pour la taille du code et c'est ŕ ce moment
lŕ que l'asm devient utile. On peut aussi désassembler le code crée
#->  par le compilo C et regarder s'il y'a lieu d'optimiser ses routines.
Je ne parlerai pas des compilateurs Pascal et Basic, ceux ci n'arrivent pas
#->  ŕ assembler de facon aussi savante que les compilos C.

A moins d'étre un trés bon codeur en asm, il est difficile d'optimiser
la vitesse des routines compilées en C. Il y'a des exceptions ŕ cette
régle, tout dépend du compilateur et des options choisies. J'ai eu
de nombreux échos de personnes qui avaient convertis leur routine
du C vers l'asm en ayant l'espoir d'un gain de vitesse considérable,
cette expérience m'est aussi arrivé. Nous nous sommes retrouvées avec
du code 25% plus lent. Un codeur de mon groupe a converti un effet
écrit en C, vers de l'asm, il a obtenu un code plus rapide que ce qu'avait
compilé Djgpp. Il faut donc tester pour savoir quelle est la meilleure
solution.

Optimiser les algorithmes

La chose qui me semble la plus importante en matiére de programmation
est l'optimisation des algorithmes. Un algorithme médiocre aura beau
étre optimisé avec la plus grande attention, le résultat final n'arrivera
pas ŕ la hauteur d'un code bien structuré et performant de part les méthodes
employées pour parvenir au résultat escompté.

Un bon algorithme n'est pas synonyme de "petite taille". Le code le plus
court n'est pas forcément le meilleur męme si le nombre de lignes de la
routine est inférieur ŕ celui d'une routine plus rapide.

Prenons l'exemple de la 3D (je simplifie l'exemple).
En 3D, on fait des rotations d'objets et on affiche les triangles ou
polygones qui constituent les objets.

1. Mauvais algorithme

        - je fais la rotation de tous les objets  (je fais tous les calculs
          de rotation, translations, homotheties...)
        - j'affiche tous les polygones (en contrôlant si ceux-ci sont sur l'écran)

2. Algorithme plus performant (mais pas optimal ;)

        - je calcule quels objets seront totalement ou en partie visible ŕ l'écran
        - je ne fais que les calculs pour ces objets
        - j'affiche seulement les polygones de ces objets

L'algorithme 1 est plus court que l'algorithme 2 car ce dernier nécessite
du code supplémentaire, pour savoir si l'objet est sur l'écran. L'algorithme
2 demeure plus performant car le temps de calcul pour savoir si un objet
sera sur l'écran ou pas, sera, dans la plupart des cas, inférieur au temps
pris pour calculer la position d'un objet qui est en définitive invisible.

### Chapitre 1b - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Les premičres instructions - Le premier programme

TUTORIAL ASSEMBLEUR - chapitre 2 - Les premičres instructions - Le premier programme

Les premičres instructions - Le premier programme

 Nous allons voir dans ce chapitre, quelques instructions.
 Ensuite nous nous attaquerons ŕ notre premier exécutable.

Les programmes nécessaires pour débuter

Pour pouvoir programmer en assembleur, il convient d'acquérir certains
  programmes qui faciliteront les tâches. Il existe différents assembleurs.
  Tout d'abord, nous avons les programmes commerciaux. La palme revient au
  Turbo Assembleur (TASM) de Borland. Ensuite nous avons le Microsoft
  Assembleur (MASM) de Microsoft. Ces programmes sont bien entendu payants
  et ne sont pas des sharewares.

Si vous voulez avoir un programme ŕ bas prix et męme gratuit, l'assembleur
  A86 avec son débogueur D86 sont les deux logiciels qu'il vous faut.

Il existe aussi NASM (netwide assembler), cet assembleur est intéressant
  pour ceux qui programment en C car il permet de linker du code en asm dans
  un programme en C. NASM est un freeware et il est trčs performant, on le
  trouve aisément sur les FTP. Il permet de faire des exécutables aussi
  séparement, cependant, il ne vaut pas TASM sur ce point lŕ.

A86 a un gros inconvénient, il ne peut générer des fichiers EXE, seulement
  des COM. Cela veut dire que vous ne pourrez faire des programmes dont la
  taille est supérieure ŕ 65k. Il y'a d'autres différences entre les EXE et
  les COM, cependant pour débuter, créer des fichiers COM est suffisant.
  A86 n'est pas capable aussi dans sa version shareware de gérér les
  instructions du 386.

Pour pouvoir écrire le code source qui contiendra les instructions, il vous
  faut un simple éditeur de  texte, le programme du DOS, Edit, convient
  parfaitement pour ce travail. Le Notepad de Windows est aussi un bon outil
  mais il est limité par la taille du fichier ŕ éditer (męme si ce problčme
  se pose rarement, il est préferable de travailler sous DOS que sous Windows
  pour éviter certains désagréments causés par Win).

Comment les utiliser ?

Je ne vais pas vous donner des fichiers d'aide sur les programmes mais je
  vais juste expliquer leur fonctionnement. En cas de problčme, il faut
  vous référer aux fichiers texte livrés avec le programme.
  Pour ce qui est de l'assembleur, avec le programme A86, il faut taper
  sous l'invite du DOS :

A86 fichier.asm (fichier.asm est le nom de votre fichier assembleur)

Cela aura pour conséquence de créer le programme "fichier.com" que vous
  pourrez exécuter. A86 peut aussi vous renvoyer un message d'erreur si
  votre programme possčde des erreurs de syntaxe, il faudra alors réediter
  le fichier source et chercher l'erreur. A86 ne vous informera PAS des
  bugs présents dans votre programme. Les bugs ne sont pas des erreurs
  de syntaxe, mais des fautes de programmation qui conduisent votre
  programme ŕ planter, ou ŕ se comporter d'une facon incorrecte.

La premičre instruction : MOV

Cette instruction vient de l'anglais "move" qui signifie déplacer mais
  attention, le sens de ce terme est modifié car l'instruction MOV ne
  déplace pas mais place tout simplement.
Cette instruction nécessite deux opérandes (deux variables) qui sont la
  destination et la source. Ceux-ci peuvent ętre des registres généraux ou
  des emplacements mémoire. Cependant, les deux opérandes ne peuvent pas ętre
  toutes les deux des emplacements mémoire. De męme, la destination ne peut
  pas ętre ce qu'on appelle une valeur immédiate (les nombres sont des
  valeurs immédiates, des valeurs dont on connait immédiatement le résultat)
  donc pas de  MOV 10,AX. Ceci n'a pas de sens, comment pouvez-vous mettre
  dans le nombre 10, la valeur de AX ? 10 n'est pas un registre.
Une autre rčgle ŕ respecter, les opérandes doivent ętre de la męme taille
  donc pas de MOV AX,AL, cela me semble assez logique. On ne peut pas
  utiliser une valeur immédiate avec un registre de segment
  (MOV ES,5 = impossible mais MOV ES,AX = possible).

Quelques exemples :

          MOV AL,CL (place le contenu de CL dans AL) donc si AL=5 et CL=10,  nous aurons AL=10 et CL=10.

          MOV CX,ES:[DI] (place dans CX, le contenu 16 bits de l'emplacement ES:[DI]).
              Donc si le word (le word est l'unité correspondant ŕ 16 bits)
              ES:[DI]=34000, nous aurons CX=34000.

          MOV CL,DS:[SI] (place dans CL, le contenu 8 bits  de l'emplacement DS:[SI]).
              Donc si DS:[SI] (attention en 8 bits) = 12, CL vaudra 12.

Au sujet de ces emplacements mémoires,il faut que vous vous les représentiez
  comme des "cases" de 8 bits, comme le plus petit registre fait 8 bits,
  on ne peut pas diviser la mémoire en emplacements plus petits.
  On peut prendre directement 16 bits en mémoire (un word) ou carrément un
  dword (32 bits). Il est tres important de saisir cette notion.
  Un exemple concret :
  - vous avez des livres sur une étagčre, le livre se trouvant toute ŕ gauche
  est le 1er de la liste, nous nous déplacons de gauche ŕ droite.
  Un livre est un byte (8 bits), 2 livres sont un word (16 bits), 4 livres
  font un dword (32 bits). Si nous faisons MOV AX,ES:[DI], nous allons
  prendre 2 livres, en commencant par le livre se trouvant ŕ l'emplacement
  DI et en prenant ensuite le livre se trouvant ŕ l'emplacement DI+1
  (l'emplacement suivant dans votre rangée de livre, 1 byte plus loin en
  mémoire).

Voici un autre exemple :

          MOV EAX,ES:[DI] (place un dword depuis ES:[DI] dans EAX). C'est comme si on copiait 4 livres dans EAX.

          Et quelques exemples incorrects :

          MOV 1,10  (impossible et pas logique)

          MOV ES:[DI],DS:[SI] (incodable malheureusement)

          MOV ES,10 (incodable, il faut passer par un registre général donc MOV ES, AX ou MOV ES,CX mais pas de MOV ES,AL).

Ce qu'il faut retenir c'est que l'instruction MOV est comme le signe '='
          MOV ES,CX c'est comme faire ES=CX.

Une autre instruction : JMP
---------------------------

          JMP est une simplification pour exprimer le mot JUMP qui signifie sauter
             en anglais.
          JMP va servir dans le programme pour "passer" d'une opération ŕ une autre,
             de sauter dans le programme ŕ différents endroits pour effectuer
             d'autrest taches. Je ne vais pas m'attarder dans des explications sur
             le pointeur d'instruction mais sachez que chaque instruction est
             désignée par un emplacement en mémoire (défini par CS:[IP]).
             L'instruction JMP va donc modifier la valeur de IP et par conséquence
             changer d'instruction.
             On peut utiliser JMP avec des valeurs immédiates.

Exemples :

          JMP 10 (il aura pour effet de passer, non pas ŕ l'instruction qui se
             trouve ŕ la ligne 10 , mais ŕ l'emplacement mémoire 10.
             Une instruction n'est pas forcément de taille 1, certaines peuvent
             s'étaler sur plusieurs bytes.)

          Il est plus simple d'utiliser des "étiquettes qu'on peut écrire sous
             la forme "ETIQUETTES:".
             Avec les étiquettes, il suffit d'écrire JMP ETIQUETTES
             pour arriver directement ŕ l'instruction qui suit le étiquette.
             Un petit exemple de code:

DEBUT :
          MOV AX,14
          JMP DEBUT

   Ce programme ne fait rien sinon placer continuellement 14 dans AX,
   on appelle cela une boucle.
   Nous verrons plus tard qu'il existe différents types de sauts.

Quelques instructions arithmétiques : ADD et SUB

ADD sert ŕ additionner, et nécessite deux opérandes : une source et une
   destination. La destination prendra la valeur de source + destination.
   Les rčgles de combinaison sont les męmes que pour MOV mais avec SUB et
   ADD, l'utilisation des registres de segment est impossible.

Exemples :

          ADD AX,CX (si AX=10 et CX=20 alors nous aurons AX=30 et CX=20)

          ADD EAX,EBX

          ADD AX,-123 (l'assembleur autorise les nombres négatifs)

          SUB DI,12

Quelques exemples impossibles :

          ADD AX,CL (comme pour MOV, un 16 bits avec un 8 bits sont incompatibles)

          SUB ES:[DI],DS:[SI]

          ADD ES,13 (impossible car c'est un registre de segment)

Notre premier programme

Nous allons faire un petit programme qui ne fera rien du tout si ce n'est
  de s'exécuter et de retourner au DOS.
  Voici le code que vous allez écrire avec l'Editeur du DOS (ou Notepad)
  et que vous allez enregistrer sous le nom de C1.ASM

          MOV AX,100
          SUB AX,10
          ADD AL,5
          RET

Aprčs avoir écrit ce texte, lancez le programme A86 en tapant "A86 C1.ASM".
  Si tout va bien,A86 devrait renvoyer un message du genre "Object : C1.COM".
  Maintenant lancez le programme C1 comme pour n'importe quel exécutable,
  rien ne se passe ? C'est tout ŕ fait normal. Notre programme calcule
  quand męme la valeur de AX mais rien ne s'affiche ŕ l'écran,
  tout se passe dans le processeur (CPU).

Maintenant étudions de plus prčs le code.
  MOV AX,100 place 100 dans AX (AX=100).
  Ensuite nous avons SUB AX,10 (AX=90) et ADD AL,5 qui va ajouter 5
  ŕ la partie basse de AX  ce qui donne AX=95.

Mais qu'est-ce donc ce RET ?
  Il s'agit d'une instruction (ret=return=retour) qui indique au proces-
  seur de rendre la main au DOS. Si vous tapez "DIR" pour voir tous les
  fichiers, vous verrez votre C1.COM de 9 bytes.
  Un des intéręts majeur de l'assembleur est la taille des fichiers crées,
  ils sont compacts, bien plus que ceux crées ŕ partir de compilateur comme
  le C, BASIC  ou PASCAL. (les compilateurs C deviennent cependant de plus
  en plus efficaces).

### Chapitre 2 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Pile - Interruptions - Second programme

TUTORIAL ASSEMBLEUR - chapitre 3 - Pile - Interruptions - Second programme

La pile (Stack) et ses instructions

La pile (stack en anglais) est un emplacement oů des données de petites
  tailles peuvent ętre placées. Cette mémoire temporaire
  (elle se comporte comme la mémoire vive mais en plus petit)
  est utilisée aussi dans les calculatrices et dans tous les ordinateurs.
  Le systčme qu'elle  employe pour stocker les données est du principe
  du "dernier stocké, premier ŕ sortir" (LIFO - last in, first out).
  Cela veut dire que lorsqu'on place une valeur (un registre, un nombre)
  dans la pile, elle est placée au premier rang (placée en priorité par
  rapport aux autres valeurs dans la pile). Lors de la  lecture de la pile
  pour récupérer la valeur, ce sera la premičre qui sera prise.
  La pile se comporte comme une pile d'assiettes ŕ laver.
  Au fur et ŕ mesure que les  assiettes sont sales, vous les empilez.
  L'assiette qui se trouve tout en bas sera la derničre  ŕ ętre lavée,
  tandis que la premičre assiette empilée sera lavée au tout début.
  Męme chose en asm, sauf les assiettes vont finir dans des registres.

Les instructions de la pile

 Nous avons tout d'abord l'instruction PUSH qui signifie Pousser.
  Cette instruction permet de placer une valeur au sommet de la pile.
  PUSH doit ętre accompagné d'une valeur de 16 ou 32 bits
  (souvent un registre) ou d'une valeur immédiate.

 Exemple 1:

          PUSH AX
          PUSH BX

 En premier lieu, AX est placé en haut de la pile et ensuite BX est
  placé au sommet, AX est repoussé au deuxičme rang.

 Exemple 2:

          PUSH 1230
          PUSH AX

 Nous plaçons dans la pile, la valeur 1230 (qui aurait pu ętre aussi
    un 32 bits) et ensuite AX, ce qui place 1230 au deuxičme rang.

 Passons maintenant ŕ l'instruction de "récupération" qui se nomme POP
 (sortir-tirer). Cette instruction demande comme PUSH, une valeur de 16
 ou 32 bits (seulement un registre).
 Elle prend la premičre valeur de la pile et la place dans le registre
 qui suit l'instruction.

 Exemple 1:

 Nous avions PUSH AX et PUSH BX et maintenant nous allons les sortir
 de la pile en utilisant
          POP BX
          POP AX

 Nous retrouvons les valeurs initiales de AX et BX mais nous aurions eu
 aussi la possibilité de faire d'abord POP AX et POP BX, ce qui aurait
 placé dans AX, la valeur BX (1er dans la pile) et dans BX, la valeur AX
 (2čme).

 Exemple 2:

 La premičre partie était PUSH 1230 et PUSH AX. Nous allons placer dans CX,
 la valeur de AX (1er de pile) et dans DX, le nombre 1230
 (2čme puisque "pilé" aprčs)

          POP CX
          POP DX

 Dans CX, nous avons la valeur de AX et dans DX, nous avons 1230.

  La pile est trčs utile pour garder la valeur d'un registre que l'on va
  utiliser afin de le retrouver intact un peu plus loin.
  Souvent dans des routines, le programmeur doit utilise tout les registres
  mais leur nombre est limité
  (utilisation des registres au maximum = gain de vitesse).
  Le programmeur va donc utiliser les fonctions de la pile pour pouvoir
  utiliser les registres et garder leur valeur actuelle pour une utilisation
  ultérieure. Malheureusement les instructions de la pile
  ralentissent le programme, la pile étant de toute façon plus lente que
  les registres qui sont au coeur du CPU.

Les interruptions - Instructions

  Dans un ordinateur, il y a plusieurs périphériques (imprimante, souris,
  modem,...). Vous connaissez certainement le BIOS qui se charge chaque fois
  que vous allumez votre ordinateur.
  Il y a différents BIOS qui dépendent des constructeurs mais ils sont
  généralement tous compatibles aux niveaux des interruptions.
  Le BIOS possčde différentes fonctions, ce sont les interruptions.
  Ces interruptions sont divisées en 7 parties principales
  (il y en a d'autres):

        - Fonctions vidéos (int 10h)
        - Fonctions disques (int 13h)
        - Fonctions de communication sur port série (int 14h)
        - Fonctions systčme - Quasiment inutiles (int 15h)
        - Fonctions clavier (int 16h)
        - Fonctions imprimante (int 17h)
        - Fonctions de gestion de l'horloge (int 1Ah)

  Vous trouverez une description plus détaillée de chaque fonction
  (elles sont nombreuses, c'est pourquoi je ne peux les détailler ici) dans
  des listes comme HelpPC de David Jurgens mais il en existe des livres
  comme La Bible PC qui en proposent. Ce type de liste est indispensable
  pour programmer, alors trouvez-en une le plus rapidement possible.

Les instructions des interruptions

  Il n'y en a qu'une qui est importante, il s'agit de INT.
  Elle permet d'appeler une des 7 catégories d'interruptions.
  Pour pouvoir séléctionner une fonction, il faut configurer les registres
  avec certaines valeurs que vous trouverez dans la liste d'interruptions
  (une derničre fois, procurez-vous en une !!!).
  Souvent, il faut juste changer AX mais parfois, tout les registres sont
  modifiés. Ensuite on appelle INT accompagné du numéro de catégorie de la
  fonction désirée.Par exemple,pour rentrer en mode texte 80x25 caractčres,
  ilfaut écrire ceci:

          MOV AX,03h
          INT 10h

  Le nombre 03 qui se trouve dans AX, signifie que nous voulons le mode
  texte 80x25 (13h est le mode 320x200x256 couleurs).
  L'instruction INT 10h appelle la catégorie 10h (fonctions vidéos).
  Et c'est tout.
  Les interruptions permettent de faire plus facilement certaines actions
  qui demanderaient une programmation plus compliquée.

Deuxičme programme

  Cette fois, le programme fait au moins quelque chose de visible.
  Il affiche un texte, le fameux Hello World.
  Tout d'abord, voici le fichier texte que vous assemblerez
  (voir précédent chapitre pour utiliser les programmes).

;-----[ PROGRAMME A COMPILER AVEC A86 ] ------

          MOV AX,03h
          INT 10h

          MOV DX,offset Message
          MOV AH,9
          INT 21h

          MOV AX,04c00h
          INT 21h

Message DB "Hello World !$"

Analysons maintenant le contenu du programme. En premier lieu, nous avons
  MOV AX,03h et INT 10h servent ŕ initialiser le mode 03h
  (mode texte 80x25 carac.). Ces instructions sont inutiles dans ce cas
  (c'est juste pour montrer) car le DOS est déjŕ en mode 80x25 mais si
  un autre mode était en vigueur avant de lancer le programme,
  on pourrait avoir de petits problčmes d'affichage sans l'initialisation
  du mode 80x25.
Ensuite nous avons le MOV DX,offset Message et MOV AH,9.
  Souvenez-vous, les instructions demandent des paramčtres.
  Le "offset Message" est le nombre correspondant ŕ l'emplacement du
  texte dans le programme (voir chapitre segment et offset).
  Il va permettre ŕ l'instruction INT 21h de trouver ce qu'il faut afficher.
  MOV AH,9 car c'est la fonction d'affichage d'une  chaîne de caractčres.
A la fin, nous avons le MOV AX,04c00h qui indique que nous voulons la
  fonction de retour au systčme d'exploitation et le INT 21h qui signale
  une interruption systčme. Le programme retourne donc au systčme
  d'exploitation en affichant le texte (retour au DOS).

Encore une petite explication sur le texte. Le mot Message est le label
  qui sert ŕ identifier le texte, on aurait bien pu le remplacer par
  n'importe quoi (Text, Blabla,...) mais il aurait fallu aussi modifier le
  MOV DX,offset Message en MOV DX,offset TEXT ou BLABLA. Lorsque
  le programme est assemblée, les caractčres sont transformés en nombre
  (leur code ascii). Les ' qui englobent le texte indique une chaîne de
  caractčres. Enfin, nous avons le symbole $, il sert ŕ indiquer la fin du
  texte, si on l'oublie, le programme ne sait pas quand la chaîne se finit,
  et des problčmes d'affichage peuvent survenir (voir meme des plantages
  dans les cas extremes).

### Chapitre 3 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Les flags - Les sauts conditionnels - CMP

TUTORIAL ASSEMBLEUR - chapitre 4 - Les flags - Les sauts conditionnels - CMP

Les flags - Les indicateurs

Les flags, "indicateurs", sont des bits qui donnent certaines informations.
  Ils sont regroupés  dans le registre de flag. Ces flags sont modifiés en
  fonction de l'exécution des différentes instructions.
  Celles-ci changent la valeur des flags selon le résultat obtenu.
  Voici une liste des différents flags et leur utilité.
  Les bits marqués du 1 ou du 0 ne sont pas utilisés.

  Bit 1 : CF
  Bit 2 : 1
  Bit 3 : PF
  Bit 4 : 0
  Bit 5 : AF
  Bit 6 : 0
  Bit 7 : ZF
  Bit 8 : SF
  Bit 9 : TF
  Bit 10 : IF
  Bit 11 : DF
  Bit 12 : OF
  Bit 13 : IOPL
  Bit 14 : NT
  Bit 15 : 0
  Bit 16 : RF
  Bit 17 : VM

  Nous n'étudierons que les 12 premiers, ce sont les plus importants.

-----------------------------------------------------------------------------
CF

Nous avons tout d'abord CF (Carry Flag). C'est le flag dit de retenue.
Dans les opérations mathématiques, il arrive que le résultat de l'opération
soit codé sur un nombre supérieur de bits. Le bit en trop est placé dans CF.
De nombreuses instructions modifient aussi CF. Par exemple,les instructions
CLC et CMC, la premičre mettant ŕ zéro CF et la deuxičme qui inverse la
valeur de CF. STC (set carry) met le flag ŕ 1.

-----------------------------------------------------------------------------
PF

Le deuxičme flag ne se modifiant pas, nous passons au troisičme qui est PF.
Il s'agit du Parity Flag. La valeur de ce flag est 1 si le nombre de bits
d'une opérande (paramčtre d'une instruction) est pair.

-----------------------------------------------------------------------------
AF

AF est l'auxiliary carry qui ressemble ŕ CF.

-----------------------------------------------------------------------------
ZF

Il s'agit du Zero Flag qui est misŕ un lorsque un résultat est égal ŕ 0.
Souvent utilisé pour les diverses opérations, il est utile pour éviter des
problčmes de divisions(je vous rappelle que diviser par zéro est impossible).

-----------------------------------------------------------------------------
SF

SF signifie Signe Flag. Simplement, sa valeur passe ŕ 1 si nous avons un
résultat signé (négatif ou positif).

-----------------------------------------------------------------------------
IF

IF pour Interrupt Flag, enlčve la possibilité au processeur de contrôler les
interruptions. Si IF=0, le processeur ne commande pas et si IF=1 alors c'est
le contraire. L'instruction STI provoque IF=1 et CLI met IF=0.

-----------------------------------------------------------------------------
DF

Le flag DF est le Direction Flag. C'est ce Flag qui donne l'indication sur
la maničre de déplacer les pointeurs (références) lors des instructions de
chaînes (soit positiviment, soit négativement).
Deux instructions lui sont associées, il s'agit de CLD et STD.

-----------------------------------------------------------------------------
OF

OF est l'Overflow Flag (indicateur de dépassement). Il permet de trouver et
de corriger certaines erreurs produites par des instructions mathématiques.
Trčs utile pour éviter les plantages. Si OF=1 alors nous avons affaire ŕ un
Overflow. Il existe une instruction qui s'occupe de ce Flag, c'est INTO qui
déclenche l'execution du code qui se trouve ŕ l'adresse 0000:0010 (int 4).

-----------------------------------------------------------------------------

C'en est tout pour les flags, vous comprendrez leur utilité au fur et ŕ
mesure de votre progression mais passons aux instructions conditionnelles
qui leur sont directement associées.

Les instructions conditionnelles

Nous abordons une partie qui est nécessaire lors de la création d'un
 programme. Souvent, le programme doit faire une action selon la valeur
 d'un résultat. Les instructions conditionnelles comme leur nom l'indique,
 sont des instructions qui font une action selon un résultat.
 Elles se basent sur les flags pour faire leur choix.
 Vous vous souvenez de l'instruction JMP, il s'agissait d'un simple saut
 vers une autre partie du programme. D'autres instructions comme JMP font
 des sauts mais selon certains critčres, on les appelle des sauts
 conditionnels. Voici la liste des ces instructions avec la valeur de
 l'indicateur nécessaire ŕ l'exécution.

-----------------------------------------------------------------------------
          JB - JNAE - JC
          Below - Not Above or Equal - Carry
          CF = 1

          JAE - JNB - JNC
          Above or Equal - Not Below - Not Carry
          CF=0

          JE - JZ
          Equal - Zero
          ZF=1

          JNE - JNZ
          Not Equal - Not Zero
          ZF=0

          JO - JNO
          Overflow - Not Overflow
          OF=1 - OF=0

          JP - JPE
          Parity - Parity Even
          PF=1

          JNP - JPO
          No Parity - Parity Odd
          PF=0

          JS - JNS
          Signed - Not Signed
          SF=1 - SF=0

          JA - JNBE
          Above - Not Below or Equal
          CF=0 et ZF=0

          JBE - JNA
          Below or Equal - Not Above
          CF=1 ou ZF=1

          JG - JNLE
          Greater - Not Less or Equal
          ZF=0 et SF=OF

          JGE - JNL
          Greater or Equal - Not Less
          SF=OF

          JL - JNGE
          Less - Not Greater or Equal
          SF (signé)=OF

          JLE - JNG
          Less or Equal - Not Greater
          ZF=1 ou SF (signé)=OF

Pour tester tout cela, nous avons besoin d'une instruction, c'est CMP qui nous aidera ŕ le faire.

L'instruction CMP

Cette instruction va nous servir ŕ tester différentes valeurs et modifier
  les flags en fonction du résultat. CMP est un SUB qui ne change ni la
  source ni la destination, seulement les flags. Un CMP BX,CX sera comme
  un SUB BX,CX ŕ l'excéption prčs que BX ne sera pas modifié.
  Si BX=CX alors BX-CX=0 donc le flag ZF sera égal ŕ 1. Si nous voulons
  faire un saut avec "égal ŕ" (JNE ou JZ qui demande ZF=1),
  nous avons ZF=1 et comme JZ demande que ZF=1 pour faire le saut,
  nous avons donc le saut.Souvenez-vous simplement que la plupart du temps,
  il s'agit de comparaisons du genre :
  effectue le saut :

          plus grand que (nombres non-signés) ---> JA
          plus petit que (nombres non-signés) ---> JB
          plus grand que (nombres signés) -------> JG
          plus petit que (nombres signés) -------> JL
          égal ŕ (signé et non-signé) -----------> JE ou parfois JZ

  il suffit de rajouter un 'n' aprčs le 'j' pour avoir la męme instruction
  mais exprimée de facon négative  ne saute pas si :

  plus grand que (nombres non-signés) ---> JNA (jump if _not above_)
  ...et ainsi de suite.

### Chapitre 4 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Utilisations des sauts conditionnels avec CMP

TUTORIAL ASSEMBLEUR - chapitre 5 - Utilisations des sauts conditionnels avec CMP - Instructions mathématiques

Utilisations des sauts conditionnels avec CMP

Les sauts conditionnels sont souvent utilisés dans des boucles.
 Ces boucles vont nous servir ŕ exécuter des actions plusieurs fois de suite.
 La boucle se divise en deux  parties :
 la partie qui fait l'action et la partie qui teste si l'action est terminée.
 Voici un petit exemple de code :

          MOV AX,0 (nous verrons que XOR AX,AX est plus petit pour le męme resultat)
          Boucle:
          INC AX
          CMP AX,12
          JE Fin
          JMP Boucle
          Fin:

 cette boucle n'est pas bien optimisée, nous pouvons faire mieux :

          MOV AX,0
          Boucle:
          INC AX
          CMP AX,12
          JNE Boucle

Nous avons enlevé 2 instructions de saut, ici, le JNE
 (jump if not equal - saute s'il n'est pas égal ŕ) va effectuer le saut
 tant que AX n'est pas égal ŕ 12, des que AX=12, il va stopper de faire
 la boucle et va faire poursuivre l'exécution du programme.
Si vous avez compris cela, vous avez ŕ peu prčs tout compris des boucles.
 On peut bien entendu remplacer le JNZ par un JNA ou une autre condition
 (voir tableau des conditions dans la leçon 4).
On peut aussi mettre plusieurs CMP dans une boucle ou faire plusieurs
 boucles dans une grande boucle.
 C'est facile mais ça vient aussi avec l'expérience. :)

Les instructions mathématiques

MULTIPLICATION : MUL / IMUL

Je vais continuer avec d'autres instructions pour compléter et vous permettre
 d'effectuer d'autres opérations. Pour l'instant,le cours est bien théorique
 mais attendez encore un peu...

Bon, commençons par la multiplication, trčs utile.
 L'assembleur utilise l'instruction MUL pour les opérations non signées et
 IMUL pour les opérations signées. MUL nécessite une opérande (un paramčtre).
 Cette opérande s'appelle la source. Le nombre qui doit ętre multiplié se
 trouve OBLIGATOIREMENT dans AX. La source est un registre (BX,CX). Si vous
 avez AX=2 et BX=5 et que vous faîtes MUL BX, vous obtiendrez AX=10 et BX=5.

Pour IMUL, c'est pareil, les assembleurs acceptent de mettre des nombres
 négatifs dans un registre (MOV AX,-10).
Pour ce qui est des nombres négatifs, sur 16 bits par exemple,
 -10 sera comme 65525 (65535-10).Le processeur soustrait ŕ la valeur limite
 du registre (ici 16 bits= 65535), la valeur absolue du nombre négatif.
 Donc pour 16 bits, les nombres signés iront de -32768 ŕ 32767. IMUL
 s'utilise comme MUL sauf qu'il faut indiquer que l'on travaille avec des
 nombres signés ou non-signés. C'est pourquoi, il faut toujours effacer
 DX (mettre DX ŕ 0) lors d'une multiplication non-signée.
 Dans le cas d'un IMUL, il faut utiliser l'instruction CWD
 (convert word to doubleword), qui 'étend' le signe de AX dans DX.
 Omettre ces instructions peut conduire ŕ des résultats erronés.
 De męme, multiplier par DX donne de drôles de résultats.

DIVISION : DIV / IDIV

 Comme pour MUL et IMUL, nous avons DIV (nombres non signés) et IDIV
  (valeurs signées). DIV divise la valeur de AX par le source.
  Comme pour IMUL et MUL, il faut que DX soit correctement paramétré.
 Donc si nous avons AX=12 et BX=5, ensuite IDIV BX,
  nous aurons AX=2 et DX=2 (DX est le reste).

 N'oublier pas qu'on ne peut pas diviser par 0 !!! Si vous passez outre,
 le programme retourne au DOS et affiche 'Div by 0'.

SHR et SHL

          SHR permet de diviser plus rapidement les registres.
          SHR demande deux opérandes: le nombre ŕ diviser et le diviseur.

          SHR divise par une puissance de deux.
            Pour diviser par deux AX,BX,CX ou DX (et les autres registres aussi),
            on met SHR AX,1 (1 parce que 2^1=2), si on veut diviser BX par 4, on fait

          SHR BX,2 (2 car 2^2=4), SHR CX,3 (2^3=8) divise CX par 8.
            Compris ? C'est trčs rapide, pour diviser AX par 256, on aura SHR AX,8.

          SHL, c'est pareil sauf qu'on multiplie : SHL AX,4 (2^4=16) multiplie AX
            par 16. On peut combiner aussi des SHL pour multiplier par une valeur
            qui n'est pas une puissance de 2.
            Si nous voulons multiplier AX par 320, on remarque que X*320 = X*256+X*64,
            or 64 et 256 sont des puissances de 2, donc nous allons multiplier par 256
            et multiplier par 64, et additionner les 2 resultats :

           MOV AX,12  ;on veut multiplier 12 (=X)
           MOV BX,AX  ;on copie dans BX (AX=X ; BX=X)
           SHL AX,8   ;on multiplie par 256 (AX=X*256)
           SHL BX,6   ;on multiplie par 64 (BX=X*64)
           ADD AX,BX  ;on additionne dans AX (AX=X*256 + BX) -> (AX=X*256+X*64=X*320)

  et voila. cependant dans certains cas, il est préferable d'utiliser MUL
  car une décomposition en puissance de 2 prendrait plus de temps...

NEG

 NEG demande une opérande : la destination. Il change le signe de la
 destination.
 Par exemple si AX=12, alors NEG AX donnera AX=-12. Si CL=-3
 alors NEG CL changera la valeur de CL en 3. Simple non ?.

Les nombres ŕ virgules

 Le problčme en assembleur, est que nous ne pouvons utiliser les nombres ŕ
 virgules directement dans les registres (sauf avec un coprocesseur mais il
 demande une programmation différente basée sur une pile).
 Nous utiliserons les nombres ŕ virgule fixe.Il s'agit simplement d'effectuer
 des calculs avec des valeurs assez grandes qui seront ensuite,
 redivisée pour retrouver un résultat dans un certain intervalle.
 Nous voudrions avoir 0.5 dans AX, mais AX n'accepte pas de nombre ŕ virgule,
 nous allons simplement multiplier AX par 256, or 256*0.5=128, parfait,
 c'est un nombre entier :

          MOV AX,128

 maintenant, nous allons multiplier 300 par 0.5 (donc par 128)

          MOV CX,300
          XOR DX,DX  ;comme MOV DX,0
          MUL CX

 dans AX, nous avons (300*128), mais ce nombre est 256x trop grand !
 il suffit de le rediviser (qui est en fait un simple decalage peu couteux
 en temps de calcul) :

 SHR AX,8

 Nous avons dans AX = (300*128)/256 = 150. Et voila ! nous avons effectué
 une MUL avec un nombre ŕ virgule fixe mais seulement en utilisant des
 entiers. Cependant, il convient d'ętre prudent avec les nombres signés et
 les grands nombres, on arrive aisément ŕ des dépassements.
 Sachez aussi que plus vous utilisez un SHL ou SHR avec une opérande
 grande, plus le résultat sera précis (plus de décimales disponibles).
 On aurait pu fort bien faire :

          MOV AX,64 ;(0.5*128)
          MOV CX,3200
          XOR DX,DX
          MUL CX
          SHR AX,7  ;AX/128

 Nous avons simplement une virgule fixe '128x plus grande'.

 ### Chapitre 5 - dake / c a l o d o x ###
 ### http://www.space.ch/scene/calodox ###


La mémoire

TUTORIAL ASSEMBLEUR - chapitre 6 - La mémoire et ses instructions

 La mémoire

 J'ai déjŕ écrit une introduction sur la mémoire de l'ordinateur dans la
  leçon 1, mais nous  allons aborder sa mise en place et voir d'une façon
  plus précise, la maničre de l'utiliser.

 Attention :
 nous travaillons pour l'instant en mode réel, le code protegé demande du code différent.

 Il existe pour cela plusieurs instructions.
  Nous avons vu que "MOV SI, offset MESSAGE" plaçait dans SI, la valeur de
  l'offset de MESSAGE. Pour une donnée dans un autre segment, il faut
  aussi utiliser le  "MOV AX,seg MESSAGE"  et ensuite  "MOV DS,AX".
  En effet, les registres ES, DS, FS et GS n'acceptent pas de valeur
  numérique directe mais seulement des registres.

 Il existe une autre instruction, LEA, qui permet de placer l'offset dans
  un registre mais en plus court.
  Par exemple "LEA SI,MESSAGE" placera dans SI, l'offset de Message.
  Privilégiez l'emploi de LEA plutôt que du MOV.
  Cela rend le code plus clair et plus compact.

 Pour pouvoir déplacer des blocs entiers de mémoire, nous n'allons pas
  lire chaque octet et le placer dans l'autre bloc mais nous employerons
  les instructions :

          MOVSB
          MOVSW
          MOVSD

 La premičre pour déplacer une (B)ytes, la deuxičme un (W)ord et la derničre
    pour déplacer un (D)words qui font 32 bits.
 Ces instructions demandent que la source (DS:SI) et l'arrivée (ES:DI)
    soient configurées.
 Il ne faut pas diriger les données vers n'importe quel emplacement de la
    mémoire. Vous planteriez ŕ coup sur le programme.
    Nous verrons plus loin, comment allouer des blocs de mémoire.

 Par exemple dans DS:SI, nous pouvons avoir les données d'une image et
    dans ES:DI, l'adresse  de la mémoire video (A000h:0000).
    Si nous voulons copier 10000 bytes, nous employerons directement le
    MOVSD (si nous travaillons avec un 386 ou plus) ou avec MOVSW.
 Pour plusieurs MOVSx, il est inutile de faire une boucle.
    L'instruction REP est lŕ pour cela.
    Il faut utiliser conjointement avec REP, le registre CX qui contient
    le nombre  d'itérations (le nombre de fois que l'on répete le MOVSx).

 Pour les 10000 bytes, nous avons :

          MOV CX,10000
          REP MOVSB

 Si nous utilisons les Words (plus rapide), c'est :

          MOV CX,5000 (un word=2 bytes)
          REP MOVSW

 Finalement, avec le 32 bits, c'est :

          MOV ECX,2500 (un dword=2 words=4 bytes) - utilisation d'un registre étendu
          REP MOVSD

 A chaque MOVSx, DI augmente de 1,2 ou 4 bytes.

 Maintenant, si nous voulons garder des données ou les placer dans un
   emplacement de la mémoire ?
   Nous utiliserons les instructions :

          STOSB
          STOSW
          STOSD

 Comme pour les MOVSx, cette instruction stocke un byte, un word et un dword.
 A la différence des déplacements,les STOSx utilisent AL, AX (EAX 32bits)
   comme valeur ŕ stocker. La valeur de AL,AX,EAX sera stockée dans ES:DI.
 A chaque STOSx, DI est augmenté de 1,2 ou 4 bytes.
   Ces instructions sont utiles pour effacer l'écran par exemple ou remplir
   un bloc mémoire avec la męme valeur.

          MOV CX,1000
          MOV AX,0
          REP STOSB

 Dans cette exemple, CX équivaut ŕ 1000, pour 1000 répétitions.
   Ensuite, le MOV AX,0 qui est la valeur qui sera placée dans le bloc
   mémoire. Pour finir, le REP STOSB qui dit que nous plaçons 10000 bytes
   de valeur 0 ŕ l'emplacement de ES:DI. En fait, ici nous placons
   AL, on ne tient pas compte de AH dans un STOSB.

 Pour réserver de la mémoire, il faut employer les interruptions, ici nous
   restons dans la mémoire conventionnelle, c'est ŕ dire que l'on ne dépasse
   pas les 640ko attribués par le DOS. Si l'on veut plus de mémoire, il faut
   utiliser les modes dits protégés ou flat.
   La programmation de ces modes est différentes et il faut avoir quelques
   notions sur le DOS  et la gestion de la mémoire.
   Si vous voulez dčs maintenant en savoir plus, lisez des documentations
   comme celles d'Intel.

 Pour la mémoire conventionnelle, on doit d'abord libérer l'emplacement
   occupé par notre programme :

          MOV BX,taille           ; la taille=(taille du prog en octet / 16)+1
          MOV AH,04ah
          INT 21h

 Ici, taille=4096 correspond 64ko. Ajustez donc la taille en fonction de
   votre programme. J'ai ajouté 1 ŕ la taille pour éviter des erreurs
   (normalement pas nécessaire, on ne sait jamais ;).

 Ensuite, il faut réserver le bloc de mémoire:

          MOV AH,48h
          MOV BX,mem_voulue       ; (mem_voulue=memoire_desiree/16)+1))
          INT 21h
          JC NOT_ENOUGH_MEM

 La mémoire voulue se calcule comme pour la libération de la mémoire.
  Le JC NOT_ENOUGH_MEMORY est utile pour détecter les erreurs.
  Si la mémoire n'est pas disponible, votre programme ira au label NOT_....
  et fera ce que vous voulez pour indiquer l'erreur (affichage d'un texte,
  retour au DOS). IMPORTANT, il faut conserver la valeur de AX aprčs avoir
  exécuté le INT 21h. AX contient le segment du bloc alloué et il
  faut garder cette valeur pour pouvoir la réutiliser lors du désallouage.

 Finalement, il faut désallouer les blocs de mémoire APRES les avoir
  utilisé (sinon quel intéręt ?).

          MOV BX,4096             ; (taille du bloc)
          MOV AX,segment_bloc     ; (le segment du bloc, nous l'avons précieusement gardé)
          MOV ES,AX
          MOV AH,49h
          INT 21h

 Le fait de ne pas désallouer de la mémoire va vous en enlever pour des
 programmes qui seraient exécutés par la suite.

 ### Chapitre 6 - dake / c a l o d o x ###
 ### http://www.space.ch/scene/calodox ###


Programmation graphique

TUTORIAL ASSEMBLEUR - chapitre 7 - Programmation graphique

 Introduction

 Nous allons aborder un vaste sujet. En effet, il existe une infinité de
   possibilités pour créer des effets avec un ordinateur.
   J'essayerais d'ętre simple et d'aller progressivement pour éviter les
   confusions. Tout d'abord, nous verrons la mémoire vidéo, les différents
   modes vidéos du BIOS. Nous ne verrons pas spécialement les autres modes
   (svga, mode-X, fakemode, truecolor..), il existe assez de docs sur ces
   modes et je m'en tiens ŕ un cours d'introduction.

 La mémoire vidéo

 Il existe un emplacement en mémoire qui s'appelle la mémoire video
   (VRAM = video-ram). Ce bloc de 64ko (en mode réel) commence ŕ partir de
   l'adresse 0a000h:0000.
 Pour pouvoir utiliser cette emplacement, il faut initialiser depuis un mode
   texte (ou un autre mode graphique), le mode video désiré.
 Nous utiliserons tout d'abord pour sa simplicité, le mode 013h de l'int 10h
   Ce mode possčde une résolution de 320x200.
   Nous aurons donc 320 pixels en largeur et 200 en hauteur.
 Le nombre de couleurs est de 256, ce qui permet déjŕ pas mal de chose.

 Dans ce mode, il suffit d'écrire la couleur du pixel (un byte) dans la
   mémoire. La position du pixel dans la mémoire n'est pas calculée selon
   les coordonnées X,Y mais linéairement. Normalement, dans un language
   comme le BASIC, on utilise l'instruction PSET (X,Y),couleur.
 En asm, on ne va pas utiliser 2 valeurs pour adresser directement dans
   la mémoire mais un seul offset.

 Il faut donc convertir ces coordonnées X,Y en adresse linéaire exploitable.
   Nous utilisons la formule :

          [(Y*320)+X]=offset_du_pixel.

   Pour mieux comprendre ce qui se passe, voici un schéma de la VRAM :

                    0                                319 (dernier pixel de la ligne)
          ligne 0 : **********************************
          ligne 1 : **********************************
                   320                              640

   Les pixels sont disposés les un aprčs les autres.
   Un  pixel ŕ la position (12,79) aura comme offset :

          (79*320)+12 = 25292

   Nous voulons un pixel de couleur 14 (couleur jaune pour la palette
   standard du DOS), nous écrirons ŕ aprčs avoir initialisé le mode vidéo et
   les registres, le byte de valeur 14 ŕ l'adresse 0a000h:025292d.

   Un petit exemple directement compilable dans A86:

          MOV AX,013h            ; nous utilisons le mode 013h => 320x200x256c
          INT 10h                ; l'int 10h utilisée pour tout ce qui est la vidéo
          MOV AX,0A000h          ; on place 0A000h dans AX que l'on va
          MOV ES,AX              ; transférer dans ES
          MOV AL,14              ; pixel index 14 => jaune
          MOV ES:[25292],AL      ; on ne peut pas écrire  MOV ES:[25292],14
          RET                    ; męme chose que MOV AX,04ch - INT 21h
                                 ; mais en plus court => fin programme

 Vous aurez un beau pixel jaune sur l'écran =8)

 Cependant, il reste un petit problčme, nous sommes restés dans le mode
   013h en revenant au DOS et nous n'avons plus de mode texte.
   La solution consiste ŕ placer avant le RET, les instructions suivantes :

          MOV AX,03h
          INT 10h

 Nous mettons d'abord 03h pour signaler que nous voulons le mode texte 03h
   (80x25) et INT 10h  pour initialiser le mode.
   Vous n'aurez plus le temps de voir votre pixel.
   Il faut ajouter avant le retour en mode texte, ces instructions :

          XOR AX,AX         ; toujours utiliser XOR pour mettre des registres ŕ 0
          INT 16h           ; interruption du clavier

 Vous aurez ainsi le temps de voir votre pixel et tant que vous ne touchez
   pas votre clavier,
 vous pourrez l'admirer, l'interruption 16h attend que vous appuyez sur une
   touche pour continuer et passer aux instructions qui suivent.
   Nous avons donc au final, le petit programme suivant :

          MOV AX,013h           ; nous utilisons le mode 013h => 320x200x256c
          INT 10h               ; l'interruption 10h utilisée pour  la vidéo
          MOV AX,0A000h         ; on place 0A000h dans AX que l'on va
          MOV ES,AX             ; transférer dans ES
          MOV AL,14             ; pixel index 14 => jaune
          MOV ES:[25292],AL     ; on ne peut pas écrire directement MOV ES:[25292],14
          XOR AX,AX             ; comme MOV AX,0 mais plus court rapide
          INT 16h           ; interruption du clavier-attente de l'appui d'une touche
          MOV AX,03h            ; mode texte
          INT 10h               ; on l'appelle
          RET                   ; retour au DOS

 Effacer l'écran

 Maintenant que nous avons quelques notions et que nous savons utiliser les
  boucles (si vous ne savez pas, retournez aux autres chapitres, c'est
  indispensable), nous allons effacer l'écran avec la couleur 0 qui est le
  noir tant que vous ne touchez pas ŕ la palette.
 En ce qui concerne celle-ci, nous verrons dans une prochaine leçon,
  comment la modifier.
 Nous avons ŕ effacer exactement 64000 bytes. Nous n'allons pas les effacer
  un ŕ un mais directement deux par deux (16 bits) ou quatre par quatre en
  32 bits. Il suffit de créer une boucle qui les effacera et regardera si
  nous sommes ŕ la fin de l'écran.
 Pour gagner la vitesse, nous utiliserons des words et des dwords au lieu
  des bytes.
 Voici un exemple :

          MOV AX,013h     ; nous utilisons le mode 013h => 320x200x256c
          INT 10h         ; l'interruption 10h utilisée pour ce qui concerne la vidéo
          MOV AX,0A000h   ; on place 0A000h dans AX que l'on va
          MOV ES,AX       ; transférer dans ES
          XOR DI,DI       ; premier pixel de l'écran
          MOV AX,0        ; on met AX ŕ 0, AL et AH = 0 = noir

          Debut:
          MOV ES:[DI],AX  ; on place directement le word ŕ l'emplacement de DI
          ADD DI,2        ; on travaille avec des Words donc on augmente de deux
          CMP DI,64000    ; on regarde si on dépasse
          JNAE Debut      ; (jmp si pas supérieur),dčs que nous avons DI>=64000, stop
          MOV AX,03h      ; mode texte
          INT 10h         ; on l'appelle
          RET             ; retour au DOS

 Nous avons vu dans le précedent chapitre,que l'utilisation de STOSB
 permettaitde remplacer des MOV,nous allons diminuer la taille du programme:

          MOV     ax,013h
          INT     010h            ; ca reste la meme chose
          PUSH    0a000h
          POP     es      ; permet de diminuer la taille d'un byte, on utilise la pile
          XOR     di,di
          XOR     ax,ax           ; met di ŕ 0 et ax=couleur ŕ 0 aussi

          MOV     cx,32000        ; nous allons effacer 32000 words = 64000 bytes
          REP     STOSB     ;(B ou W) MOV ES:[DI],ax ¦ ADD DI,2 ¦ et ceci, 32000 fois.
          MOV     al,03h          ; pas besoin d'initialiser AX ŕ 03, AH est déjŕ ŕ 0
          INT     010h            ; on initialise que AL et on appelle l'interruption
          RET

 C'est de cette maničre que l'on procčde dčs que l'on optimise du code pour
 la taille (intros 4k..). Faites attention aux bugs lors des optimisations.

### Chapitre 7 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Le vocabulaire en matičre de graphisme

TUTORIAL ASSEMBLEUR - chapitre 7B - Le vocabulaire en matičre de graphisme

Pixel

 Le pixel (point,pel) est la plus petite partie qu'un écran peut afficher.
 L'ordinateur restitue l'image avec un ensemble de points, les pixels.
 Chaque pixel peut ętre modifié si l'on est en mode graphique.
 Le mode texte quant ŕ lui, affiche des "blocs" de pixels qui représentent
 les caractčres (il est cependant possible de modifier les caractčres).

La résolution

 La résolution permet de savoir combien de pixels sont affichés en męme
  temps ŕ l'écran.Une résolution de 320x200 représente 320 pixels en largeur
  et 200 en hauteur, ce qui fait 64000 points au total.

 Les résolutions les plus courantes sont 320x200, 320x256,640x480, 800x600,
  1024x768. A noter que le mode texte du DOS est de 80x25 (80 caractčres en
  largeur et 25 en hauteur) mais représente, une résolution de 640x300.
 Il est possible d'obtenir des résolutions beaucoup moins courantes mais
  avec le risque  de rencontrer des incompatibilités matérielles.

Les bits par pixel (BPP)

 Plus le nombre de bits par pixel est elevé, plus le nombre de couleurs
 disponibles est grand. En 256 couleurs, nous avons 8bpp = 256 couleurs
 (1 byte). En 24 bits, 3 bytes définissent la couleur (Rouge, Vert, Bleu).
 On distingue aussi des modes 15 bits, 16 bits (65536 couleurs),
 32 bits (RGB + un canal alpha/transparence, le nombre de couleurs demeure
 le męme que pour du 24 bits) mais aussi 1 bit (noir ou blanc),
 4 bits (16 couleurs).

La palette

 En 256 couleurs, il faut créer une palette qui correspondra aux teintes
   désirées parmi 262144 (64*64*64).
   Pourquoi 64 ? Et bien parce que la carte vidéo demande les  valeurs de
   rouge, de vert et de bleu. L'intensité minimale est 0 et le maximum 63.
   Le noir est 0,0,0 et le blanc 63,63,63.
   Le rouge sera 63,0,0. En utilisant une caractéristique du VESA
   (norme présente sur les cartes SVGA, on peut étendre cette limite de
   0 ŕ 255 pour plus de précision, c'est ce que fait Windows).

 Le rayon cathodique, le retour de balayage

 Le rayon cathodique parcourt votre écran et envoie un faisceau d'une
   certaine luminosité contre le revętement de l'écran.
   Cela produit l'affichage d'un point lumineux.
 L'image est dessinée ligne par ligne, de gauche ŕ droite et de haut en bas.
   Le rayon parcourt l'écran et lorsqu'il arrive ŕ la fin d'une ligne,
   il effectue un retour de balayage horizontal. De męme en bas ŕ droite,
   le rayon finit son trajet avec un retour de balayage vertical.
   On ne doit pas modifier le contenu de l'écran pendant que le rayon trace
 l'image mais on doit attendre le retour du faisceau (retrace).
   Cela provoque des scintillements et des effets peu esthétiques comme des
   coupures. Il vaut mieux modifier l'image pendant un retour vertical car
   celui-ci est beaucoup plus lent que le balayage horizontal.
   Il en va ainsi avec les modifications de palette pendant l'affichage.

Les registres de la carte VGA

 La carte VGA ressemble un peu ŕ un processeur. Elle contient des registres
   qui peuvent ętre modifiés. On peut ainsi agir sur la résolution,
   la palette, le nombre de couleurs, la taille des pixels, contrôler l'état
   du balayage, le timing (ŕ utiliser prudemment)...

Le CRTC / Cathod Ray Tub Controller

 Le contrôleur du tube de rayons cathodiques.
 Il prend en charge la création du signal vidéo.
 On peut facilement le programmer mais il faut veiller ŕ ne pas faire
 n'importe quoi. On risque męme d'endommager l'écran.

Le TS / Timing Sequencer

 Le séquenceur de timing s'occupe de la gestion de la mémoire vidéo,
   des plans et des  caractčres en mémoire.
   Le rafraîchissement de la mémoire se fait grâce au TS.

Le GDC / Graphic Data Controller

 Le contrôleur de données graphiques permet d'atteindre d'autres fonctions
   inaccessibles avec le TS. Il s'occupe des octets lors de la lecture ou
   de l'écriture.Il gčre les accčs ŕ la mémoire vidéo et s'occupe des plans.

L'ATC / Attribute Controller

 Ce circuit est le contrôleur d'attribut.
 Il est responsable de la gestion de la palette et des couleurs.

Le DAC / Digital to Analog Converter

 Dans le DAC, les octets sont directement transformés en données utilisables
   par l'écran. Ils sont changés en signaux qui comportent les valeurs de
   rouge, vert et bleu. Il gčre aussi les lectures et les reconversions.

 Actuellement, on n'utilise quasiment plus ces "registres" ŕ l'exception du
   DAC qui permet de modifier la palette. Il arrive parfois d'utiliser les
   autres registres pour du mode texte bidouillé et les modes-X.

### Chapitre 7b - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


La carte vidéo

TUTORIAL ASSEMBLEUR - chapitre 8 - La carte vidéo

La carte vidéo

Nous avons vu dans le chapitre 7, la présence d'une zone de la mémoire
  appellée mémoire vidéo.
Il s'agit lŕ seulement de la partie "visible" du processus de génération
  d'image. En effet, le point avant d'ętre affiché sur votre écran doit
  passer par une série d'étape. Le périphérique qui s'occupe de tout cela,
  est la carte vidéo ou carte graphique. Celle-ci permet de contrôler
  l'écran,sa résolution, la palette de couleurs, la vitesse de balayage ...

Il convient de faire la distinction entre plusieurs types de cartes :

          CGA  : 4 couleurs avec 320x200 et noir/blanc avec 640x200... On oublie...
          EGA  : 16 couleurs avec 640x350 avec d'autres modes.Il reste quelques EGA
          VGA  : 256 couleurs avec 320x200 et 16 couleurs en 640x480.
                     Des modes textes et d'autres résolutions
          SVGA : 16 millions de couleurs, 1024x768 au maximum

Tout le monde possčde du SVGA désormais. Les nouvelles cartes reprennent
toujours les modes des anciennes,on a donc des modes CGA sur une carte SVGA

La palette

 Une image n'a pas forcément les męmes types de teintes qu'une autre.
  C'est un problčme qui n'apparaît qu'avec les modes possédant 256 couleurs
  et moins. Il faut donc adapter l'écran ŕ notre image pour éviter d'avoir
  un résultat décevant. Il existe plusieurs méthodes qui passent soit par
  le BIOS ou les registres de la carte vidéo.
  On commence avec la plus simple mais la plus lente aussi. On ne peut avec
  cette méthode que modifier une couleur entičre mais un des ses composantes
  de facon separee. Le code vient d'un tutorial de VLA :

          MOV AX,CS
          MOV ES,AX
          MOV DX,offset Palette
          MOV AX,1012h
          XOR BX,BX
          MOV CX,256
          INT 10h

Tout d'abord, le MOV AX,CS car ES doit valoir CS. En tout cas, sans les
  deux premičres lignes, ça plante (bizarre ;)...
  Ensuite, MOV ES,AX donc ES=CS. Nous nous plaçons dans les données de la
  palette avec MOV DX,offset Palette, on aurait pu faire aussi un LEA DX,
  Palette. Nous mettons dans AX, les nombres qui nous permettent de choisir
  l'embranchement 10h et le sous-embranchement 12h de l'interruption 10h.
  L'embranchement 10h permet de modifier les couleurs et le 12h porte la
  mention "set block of DAC color registers". Si vous possédez Help-PC de
  Jurgens,vous n'aurez pas de problčmes ŕ trouver ces différentes fonctions.

Le XOR BX,BX indique que nous commençons ŕ la couleur 0 et le MOV CX,256
  indique l'écriture de 256 couleurs donc l'ensemble de la palette.
  On finit avec le INT 10h pour modifier le tout. La palette doit ętre
  donnée en indiquant les 3 composantes rouge, vert et bleu, dans l'ordre.
Il faut trois octets par couleur, ce qui fait 768 octets pour la palette
  finale. N'oubliez pas que cette méthode est ŕ utiliser que lorsqu'on
  affiche une image non animée. Sa lenteur empęche la possibilité de
  l'implanter dans des animations. Nous passons ŕ un stade supérieur.
  Nous allons directement modifier les registres de la carte vidéo.
   (Code venant du Tutorial toujours de VLA) :

          MOV AX,CS
          MOV DS,AX
          MOV AL,0
          MOV DX,03C8h
          OUT DX,AL
          INC DX
          MOV SI,offset Palette
          MOV CX,256*3
          REP OUTSB

Le MOV AL,0 pour dire que la premičre couleur est 0. Le MOV DX,03C8h donne
  une indication pour l'écriture des couleurs dans la DAC.
  Le OUT est l'instruction qui permet d'envoyer ŕ l'adresse d'un périphé-
  rique (adresse=DX), la valeur de AL. Ensuite, nous faisons un INC DX qui
  augmente DX de 1 et le fait passer ŕ la valeur 03c9h qui est l'écriture
  elle-męme. $ Ensuite, SI se place sur la palette. Nous mettons dans CX,
  256*3, 768 pour dire qu'il y a 256 couleurs avec 3 composantes. Et enfin,
  le REP OUTSB qui s'apparente au MOVSB mais avec des ports de périphériques.

Si l'on veut modifier une couleur, on peut utiliser la méthode qui suit :

          MOV DX,03c8h
          OUT DX,AL
          MOV DX,3c9h
          MOV AL,AH
          OUT DX,AL
          MOV AL,BL
          OUT DX,AL
          MOV AL,BH
          OUT DX,AL

Il faut,avant de lancer cette routine,placer dans AL l'index de la couleur.
  Dans AH, la composante rouge. Dans BL, la composante verte et dans BH,
  la composante bleue.
Comme avant, nous initialisons l'écriture avec le 03c8h.
  Nous avons dans AL, l'index qui est envoyé pour dire que c'est cette
  couleur qui est modifiée. Ensuite, nous avons le 3c9h qui écrit les
  composantes (toujours par le biais de AL) grâce aux OUT's. Vous pouvez
  dčs maintenant modifier la couleur 0 dans un programme, vous verrez le
  noir se changer dans la couleur que vous avez choisi.

  ### Chapitre 8 - dake / c a l o d o x ###
  ### http://www.space.ch/scene/calodox ###


Le starfield - Les fichiers de données

TUTORIAL ASSEMBLEUR - chapitre 9 - Le starfield - Les fichiers de données

Le starfield - Les fichiers de données

Je ne sais pas qui est le premier ŕ avoir crée le concept du champ d'étoile
- starfield.Le principe est simple mais il se classe en deux catégories.
La premičre est celle des starfields 2D -  les étoiles se
déplacent latéralement de gauche vers la droite ou vice-versa. Bref, en 2D.
Le second type de starfields est ceux qui utilisent la perspective.
Les étoiles viennent du fond de l'écran et se dirigent vers nous en donnant
l'impression aux spectateurs de voyager dans l'espace. Le meilleur exemple
est le starfield économiseur d'écran de Windows. Il existe des variantes de
ces effets. Les étoiles peuvent ętre remplacées par des sprites qui
grossissent au fur et ŕ mesure de leur rapprochement.

Dans ce chapitre, nous verrons la conception d'un starfield en 3D
utilisant la perspective et des points en guise d'etoiles.

Le code pour A86

Le fichier qui suit ne peut pas ętre compilé comme tel.
Il s'agit juste du programme principal mais pour pouvoir ętre exécuté,
il faut que vous dezippiez STRFLD.zip. Ce fichier contient l'image de
fond, le code ASM et le fichier COM précompilé.

Les explications sur le fonctionnement du starfield se trouvent aprčs
cette séquence de code.

          mov ax,013h                  ;MODE 013h
          int 10h
          push 0a000h                  ;on se place sur la vidéo
          pop es

          call CLS                     ;on appelle la procédure qui affiche l'image de fond

          Initialisation:              ;on va créer les coordonnées de nos étoiles
          xor bp,bp                    ;bp sert de compteur
          lea si,stars                 ;on se place sur l'adresse de STARS
                                       ;qui se trouve maintenant en DS:SI

          Coordo:                      ;cette routine trouve des valeurs aléatoires
                                       ;pour les coordonnées X,Y,Z des étoiles
                                       ;X,Y,Z sont des WORDS !!!

          mov dx,320                   ;valeur X maximale => 320
          call RND                     ;on cherche un nb aléatoire

          mov ds:[si],dx               ;qui sort dans DX et que l'on place au premier emplacement
                                       ;de ds:[si]

          mov dx,200                   ;le Y => max. 200
          call RND                     ;un petit nombre aléatoire
          mov ds:[si+2],dx             ;qu'on place ŕ DS:[SI+2] car on travaille avec des WORDS
                                       ;(si c'était des bytes, on aurait DS:[SI+1])

          mov dx,700                   ;la taille Z est fixe, toutes les étoiles sont ŕ la męme
                                       ;distance de l'observateur
          mov ds:[si+4],dx             ;DS:[SI+4] car c'est un word
          add si,6                     ;on augmente SI de 6 car 3 words = 6 bytes
                                       ;et la plus petite valeur d'incrémentation de S
                                       ;est le byte

          inc bp                       ;on dit ŕ BP qu'on a fait une étoile
          cmp bp,100                   ;est-ce qu'on a nos 100 étoiles ?
          jna Coordo                   ;si on les a pas, on continue

          Go_Etoiles:
          xor bp,bp                    ;BP est un compteur comme toujours
          lea si,stars                 ;on se place sur les données des étoiles

          Start:                       ;dans cette routine, on transforme les coordonnées 3D
                                       ;en 2D

          mov ax,ds:[si]               ;dans AX, le X de l'étoile
          mov bx,ds:[si+4]             ;dans BX, le Z de l'étoile
          shl ax,9                     ;on multiplie AX par 512
          cwd                          ;CWD ! Nombre signé
          idiv bx                      ;IDIV car nombre signé
                                       ;ŕ ce stade, nous avons (512*X)/Z
          mov w[XT],ax                 ;c'est la position X de l'étoile en 2D

          mov ax,ds:[si+2]             ;on fait pareil avec Y
          shl ax,9                     ;multiplié par 512
          cwd
          idiv bx                      ;Z est resté dans BX donc pas besoin de le recharger
          mov w[YT],ax                 ;position Y de l'étoile en 2D

          add w[XT],160                ;nous devons centrer par rapport ŕ l'écran
          add w[YT],100                ;car les coordonnées étaient relatives ŕ la position (0,0)

          cmp w[YT],200                ;quelques tests pour voir si le pixel ne dépasse pas de l'écran
          ja No_Show                   ;YT>200 = pas de pixel
          cmp w[XT],320
          ja No_show                   ;XT>320 = pas de pixel

          Trace:                       ;Ici, nous calculons l'offset du pixel sur l'écran
                                       ;car nous n'avons que des coord. X et Y.

          mov ax,w[YT]                 ;on prend Y
          mov bx,320                   ;on multiplie par la largeur (Y*320)
          mul bx                       ;voila...
          add ax,w[XT]                 ;et on rajoute le X
          mov di,ax                    ;cet offset part chez DI qui pointe sur l'écran

          mov al,14                    ;couleur 14 de la palette standard du DOS = jaune
          mov es:[di],al               ;et on place le point dans la vidéo (stosb marche aussi)

          No_Show:                     ;ce label se trouve lŕ, si jamais l'étoile était en dehors
                                       ;de l'écran
          in al,60h
          cmp al,1                     ;test pour la touche "ESC"
          jz Endo

          sub ds:w[si+4],4             ;si on a pas de touche ESC, on réduit la distance de l'étoile
                                       ;par rapport ŕ l'observateur
          cmp ds:w[si+4],0             ;on teste si on a zéro dans Z car si on fait (X/Z) avec Z=0
                                       ;on obtient un DIVID ERROR (on ne peut pas diviser par 0 !!!)

          jz Restart                   ;si jamais on avait zéro et bien on remet le Z de l'étoile ŕ 500

          Resto:
          add si,6                     ;on augmente SI de 6 car on passe ŕ l'étoile suivante
          inc bp                       ;on augmente le compteur d'étoiles
          cmp bp,100                   ;on les a toutes faites ?
          jz ReS                       ;alors il faut effacer l'écran avec l'image et recommencer

          jmp Start                    ;autrement, on continue

          Res:
          call CLS                     ;la proc. CLS efface l'écran avec l'image
          jmp Go_Etoiles               ;et on fait la nouvelle vague d'étoiles qui est plus proche
                                       ;maintenant

          Endo:                        ;si on a la touche "ESC" et bien on quitte
          mov ax,03h
          int 10h                      ;par le mode texte et un RET qui retourne au DOS
          ret

          Restart:                     ;on vient voir RESTART si le Z de l'étoile est égal ŕ 0
          mov ax,700                   ;on remet ŕ 700
          mov ds:[si+4],ax             ;le Z de l'étoile en cours de calcul
          jmp Resto                    ;et on retourne d'oů on est venu

          SYNC PROC
          mov dx, 3DAh
          retrace1:
          in al, dx
          test al, 8
          jz retrace1                  ;attente du retour du faisceau lumineux.
          retrace2:
          in al, dx
          test al, 8
          jnz retrace2
          ret
          SYNC ENDP

          CLS PROC
          lea si,hello
          xor di,di
          mov cx,32000
          rep movsw
          ret
          CLS ENDP

          RND PROC         ;voir code du feu - chapitre 9 - pour plus de commentaires
          random: push ax
          push dx
          mov ax,31421
          mov bx,w[seed]
          mul bx
          add ax,6927
          mov seed,ax
          pop dx
          mul dx
          add dx,1
          pop ax
          ret
          RND ENDP

          seed    dw 0                   ;donnée du créateur de nb aléatoires
          include hello.db               ;une image dans un fichier externe est
                                         ;liée ŕ notre fichier principal

          stars dw 300 dup(?)            ;300 words (300/3=100 étoiles) dont la valeur
                                         ;est (?) => inconnue - DUP sert ŕ dire
                                         ;que nous voulons une suite de DW ou DB

          xt dw ?                        ;les WORDS qui sont XT avec valeur inconnue
          yt dw ?                        ;et YT avec valeur inconnue (le ? indique
                                         ;une valeur inconnue)

Les procédures

En observant le code, vous aurez probablement aperçu les parties du programme
qui commencent par des PROC et finissent par des RET et ENDP. Il s'agit des
procédures. Ce sont des mini-programmes qui effectuent un travail lorsqu'on
les appelle avec un CALL. Pour déclarer une procédure, il faut d'abord créer
une ligne avec les instructions :

PROC Nom_de_la_Procédure
Par exemple : PROC Additionne

Ensuite, vous placez les différentes instructions nécessaires et lorsque
vous avez fini, vous mettez l'instruction RET, par exemple :

          PROC Additionne
          MOV AX,14
          ADD AX,BX
          RET

Il ne faut pas oublier de mettre aprčs le RET, la commande ENDP
(End of Procedure) :

          PROC Additionne
          MOV AX,14
          ADD AX,BX
          RET
          Additionne ENDP

Voila, pour appeller votre procédure, vous faites un CALL
Nom_de_la_procédure. Ici, ce sera CALL Additionne. Attention, le RET de la
procédure, redonne la main au programme et ne le fait pas revenir au DOS.
Si vous voulez utiliser une procédure pour sortir du programme, il faudra
utiliser MOV AX,04ch - INT 10h.

Comment ça marche ?

L'effet du starfield repose sur le principe de la perspective. Plus un objet
est éloigné, plus il est petit. Le programme doit donc considérer cela et
non pas simplement afficher les étoiles ŕ l'écran. Pour avoir cette
troisičme dimension, qu'est la profondeur, nous introduisons en plus
des coordonnées X et Y habituelles, la coordonnée Z qui indique la distance
de l'étoile par rapport ŕ l'observateur.

Pour passer de coordonnées 3D vers des positions X,Y en 2D, il faut
appliquer les formules suivantes :

          X_2D = (Facteur*X_3D)/Z_3D
          Y_2D = (Facteur*Y_3D)/Z_3D

C'est trčs simple, nous multiplions le X (coordonnée 3D) de l'étoile par un
facteur qui doit ętre naturellement une puissance de 2, afin de faciliter
les calculs. Pour un écran de 320x200, les valeurs 256 ou 512 sont un bon
choix. Ensuite, nous divisons le X*facteur par le Z de l'étoile.
Nous obtenons la position X sur l'écran. Nous faisons de męme avec Y.
Attention au Z negatif et egal a 0. Il faut absolument eliminer les
etoiles se trouvant a de telles positions. Elles conduiraient a des
resultats errones.

Pour trouver l'offset dans la mémoire vidéo, nous appliquons la formule
déjŕ connue :

          OFFSET_MEMOIRE=(Y*320)+X

Il ne reste plus qu'ŕ placer un point coloré ŕ cet emplacement.
Nous rapprochons les étoiles en enlevant ŕ leur Z, une valeur
multiple de 2 (afin d'arriver pile ŕ zéro). Si nous arrivons ŕ 0, il
faut arręter de calculer cette étoile car nous ne pouvons diviser
pas 0. Et on recommence en remettant la valeur Z telle qu'elle était au
début.

Utilisation des images

Au lieu de bętement effacer l'écran ŕ chaque nouvelle "avancée" des étoiles,
j'ai inclus un fond qui représente une image (hmmm...pas terrible, resultat
de 2 secondes  sous Paint Shop Pro).
La taille est évidemment de 320x200 pour recouvrir l'ensemble de l'écran.
Elle a été sauvegardée sous le format "RAW" ou format BINAIRE. Ce format
retranscrit directement les couleurs en octets. Par exemple, un suite de
pixels comme "15 10 13" sera enregistrée sans changement ni compression.
Cela nous donne une image de 320*200=64000 bytes. Il faut ensuite convertir
cette image au format que j'appelle BYTES ou WORDS. Cette conversion
transforme les suites de BYTES binaires du fichier en fichier composé de DB
ou de DW afin que l'assembleur puissent les comprendre. J'utilise le
programme INC-PRO III.

Pour obtenir l'aide, il faut taper "INCPRO /?". Les conversions s'opčrent
trčs facilement (ex : INCPRO image.raw image.db image). Le dernier paramčtre
est dans cet exemple, le label.( label = offset image du fichier include )
Il ne reste plus qu'ŕ introduire le
paramčtre "INCLUDE" dans votre programme principal pour dire que vous avez
un fichier de donnée externe qu'il faut inclure dans le fichier principal
lors de la compilation. Cela donnera par exemple "INCLUDE IMAGE.DB". Ensuite,
cette image sera accessible comme n'importe quelle autre donnée.
Un "MOV AX,OFFSET IMAGE" sera tout ŕ fait valable et il chargera la
valeur de l'offset de votre image.

Les fichiers COM sont cependant astreints ŕ une taille de 64k au m
aximum (65535 bytes). Ils ne peuvent pas "aller" dans un autre segment
(souvenez-vous qu'un segment=64k). Nous pouvons donc juste stocker une
image de 320x200. Il nous reste ainsi un peu d'espace pour notre code et
d'autres données. Les EXE résolvent le problčme mais ils sont plus
difficiles ŕ gérer (en tout cas avec A86, TASM evite bien des problemes
mais le mieux est le passage au mode protege).

Le code source du starfied ne devrait pas poser de problčmes,
cependant je vais quand męme expliquer l'affichage de l'image.
Nous nous plaçons tout d'abord sur l'adresse de l'image avec "LEA SI,HELLO".
HELLO est le label de l'image. Le programme place l'adresse dans DS:SI.
Nous mettons DI ŕ zéro car nous voulons afficher l'image dčs le début de
l'écran bien entendu. Ensuite, nous mettons dans CX, la valeur de 32000.
CX sert de compteur pour l'instruction REP (voir chapitre sur la
mémoire). Nous faisons un REP MOVSW, qui copie des WORDS de DS:SI vers ES:DI.
Nous avons mis 32000 car nous travaillons avec des WORDS et c'est plus
rapide que les BYTES. Cependant avec les bytes, nous aurions "MOV CX,64000
et REP MOVSB". On peut copier des DWORDS mais pas avec A86 qui se limite
aux instructions 16 bits (il existe une astuce, nous la verrons dans un
prochain chapitre).

L'instruction DUP

Un autre type de donnée est celle qui utilise les DUP. Il s'agit d'une
simple représentation pour décrire une série de BYTES ou
de WORDS. Cela permet d'économiser du temps au lieu de taper
300 fois "DB ?" ou "DB 200". La syntaxe est :

          Nom_Donnee  /  DB/DW  /  Nombre_Répetitions / DUP / (Valeur)

par exemple, nous voulons 300 bytes de valeur 12 :

          DONNEE db 300 dup(12)

pour 300 words de valeur 12 :

          DONNEE dw 300 dup(12)

si nous voulons 300 words, d'une valeur pouvant ętre modifiée (comme un tableau) :

          DONNEE dw 300 dup(?)

vous pouvez faire de męme avec des BYTES.

Cette instruction permet de rendre le code plus clair mais cela
n'empęche pas que l'assembleur va la considérer comme une
suite de données. La taille du fichier sera donc la męme que si vous aviez
tapé ŕ la main les 300 "DB ?".

Notre starfield aura une taille de plus de 64000 bytes car il contient le
code + l'image + quelques autres données. Pour rendre plus petit
l'exécutable, il faut utiliser un compresseur d'EXE ou de COM. PKLITE est
une bonne réference męme s'il n'est pas le plus performant. Il permet de
compresser les fichiers COM et EXE avec parfois plus de 80 %.
Dans notre cas, la compression sera efficace car l'image est trčs simple et
comporte peu de variations.

### Chapitre 9 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Image format PCX - Effet de Fade-Out

TUTORIAL ASSEMBLEUR - chapitre 10 - Image format PCX - Effet de Fade-Out

Le format PCX

Nous avons vu dans le précedent chapitre, la manipulation des images en
format RAW, c'est ŕ dire binaire. Cependant, il est intéressant de jeter un
coup d'oeil vers les images dites compressées. Les formats sont nombreux et
leurs performances sont diverses. Un des formats les plus simples est le
format PCX. Celui-ci a été crée par ZSOFT pour alléger la taille des images.
L'algorithme est basé sur le RLE (Run Length Compression). La compression
agit sur les répetitions que l'on peut trouver dans le fichier en écrivant
simplement le nombre de répetitions et la couleur du pixel. On peut
économiser ainsi jusqu'ŕ 90% de la taille du fichier suivant sa complexité.

Le header du format PCX (l'en-tęte)

Le header est la partie qui se trouve au début du fichier. Il donne des
informations sur la taille de l'image, le nombre de couleurs, la palette...
ZSOFT a crée plusieurs types de format PCX - le format 5 est celui destiné
aux images de 256 couleurs. Je passe sur les formats 16 couleurs et
2 couleurs, ils sont presque devenus inutiles.

Voici la composition de cette en-tęte :

          Octet 0-1                Version et fabricant (le plus souvent 5 ou 10)
          Octet 2                  Type de compression (0 = non compressé / 1 = compressé)
          Octet 3                  Nombre de bits pour désigner un pixel, 256 couleurs = 8

          Octet 4-5                Valeur de X au minimum
          Octet 6-7                Valeur de Y au minimum
          Octet 8-9                Valeur de X au maximum
          Octet 10-11              Valeur de Y au maximum
                                   (largeur = XMAX-XMIN+1) (hauteur = YMAX-YMIN+1)

          Octet 12-13              HDPI - Résolution horizontale en DPI
          Octet 14-15              VDPI - Résolution verticale en DPI
          Octet 16-65              Paramčtres palette
          Octet 64                 ??? - Doit ętre égal ŕ 0
          Octet 65                 Nombre de plans
          Octet 66-67              Nombre d'octets pour une ligne (doit ętre pair)
          Octet 68-69              Interprétation Palette / 1=Couleur ou Noir-Blanc 2=Gris

          Octet 70-71              Taille horizontale écran en pixels
          Octet 72-73              Taille verticale écran en pixels
          Octet 74-127             Remplissage par des 0 pour arriver ŕ un header de 128 bytes.

Ce header n'est vraiment utile que si vous voulez créer un viewer qui
doit s'adapter selon la taille de l'image. Dans les programmes en ASM de
cette page, on ne soucie pas du header car l'on connait déjŕ la taille de
l'écran (320x200) et nous savons que l'image est en 256 couleurs.
Pas de problčmes, on oublie le header...et on passe ŕ la suite.

Les données compressées et la palette

Comme nous savons que les premiers octets sont constitués du header,
nous allons les sauter et arriver ŕ l'octet 128 du fichier. Nous allons lire
les octets. Si l'octet en cours de lecture est inférieur ŕ ou égal ŕ 0C0h,
alors il s'agit d'un pixel non compressé. Nous plaçons sa valeur
(sa couleur) directement ŕ l'écran et nous passons ŕ l'octet suivant.

Si l'octet lu est supérieur ŕ 0C0h, alors nous avons affaire ŕ une
compression. Il faut soustraire 0C0h ŕ la valeur de l'octet, ainsi nous
obtenons le nombre de répetitions. L'octet qui suit l'octet de compression,
est la couleur du pixel ŕ répeter. Nous copions X fois (valeur de l'octet de
répetition) le pixel de couleur. Pour ętre un peu plus clair, voici
l'algorithme en pseudo-code :

Position_Fichier=128
Position_Ecran=0

          1. Lire octet A1 ŕ la position_fichier

          2. Si A1 est inférieur ou égal ŕ 0C0h alors rajouter pixel ŕ l'écran
              et Position_Fichier=Position_Fichier+1
              et Position_Ecran=Position_Ecran+1
              Aller au point 4

          3. Si A1 > 0C0h alors on a une compression
              Nb_Repet = A1-0C0h
              Lire l'octet A2 ŕ la  Position_Fichier+1 (l'octet qui suit)
              Rajouter A1 fois, le pixel de couleur A2 dans l'image
              et Position_Fichier=Position_Fichier+2
              et Position_Ecran=Position_Ecran+A1

          4. Recommencer jusqu'ŕ ce que Position_Ecran = 64000 (avec image de 320x200)

Voila, nous avons le contenu de l'image aprčs décompression. Cependant,
il vaut mieux paramétrer la palette avant d'afficher l'image. Il faut
aller ŕ la fin du fichier et "reculer" de 768 octets pour se positionner au
début des données de la palette. Le format de la palette est le
classique R,V,B (rouge-vert-bleu). Chaque composante est codée sur
8 bits (0-255) mais la carte VGA n'accepte que 6 bits. Donc il faut
diviser chaque valeur par 4 pour arriver aux 6 bits.
On utilise bien entendu, SHR XXX,2.

Le code pour A86

L'exemple complet est present dans PCXEX.zip.
Pour l'instant, il ne peut que lire les fichiers qui sont convertis du
format binaire vers le format DB ou DW (comme l'image du starfield).
Nous verrons certainement plus tard, la gestion des fichiers.
L'image ŕ lire doit faire 320x200 pixels et doit avoir une palette qui
s'étend sur 256 couleurs... Bon, voici le code principal sans les données
de l'image ŕ proprement parler - les parties déjŕ vues sont moins
commentées - si vous avez des lacunes, allez voir aux
chapitres qui précčdent :

          Start:
          mov ax,013h
          int 10h
          push 0a000h
          pop es                       ;Mode 320x200 - ES:DI = mémoire vidéo

          Debut_Palette:
          lea si,fin_image             ;On se place au début de la palette
          sub si,768                   ;qui se trouve 768 bytes avant la fin du fichier

          Pal_PCX:                     ;Nous allons lire les données de la palette
          xor ax,ax
          xor cx,cx                    ;Mise ŕ zéro pour éviter que cela ne perturbe
                                       ;le déroulement de la routine
          Make_Pal:
          mov ah,ds:[si]               ;Bon, on prend tout d'abord la composante ROUGE
          mov bx,ds:[si+1]             ;Ensuite, le VERT et le BLEU directement en un WORD
                                       ;c'est plus court que de les prendre séparement

                                       ;ŕ ce stade : AL=index_couleur AH=R BL=V BH=B

          shr ah,2                     ;Ok ! On continue en divisant chaque composante
          shr bl,2                     ;par quatre car la carte n'accepte que 6 bits
          shr bh,2

          call SET_COLOR               ;On met la couleur en place
          inc al                       ;On va vers index  suivant
          add si,3                     ;On augmente notre position dans le fichier de 3
                                       ;puisque une couleur=3 bytes
          cmp si,fin_image             ;Et on regarde si on est ŕ la fin du fichier
          jnz Make_Pal                 ;On recommence si on a pas fini

                ****************

          Affiche_1:                   ;Maintenant, on va afficher l'image
          lea si,debut_image+128       ;On se place aprčs le header qui nous sert ŕ rien
          xor di,di                    ;Et au début de l'écran enfin...de la mémoire vidéo

          Affiche_2:                   ;Nous prenons un octet du fichier
          mov al,ds:[si]               ;dans AL
          cmp al,0c0h                  ;et nous regardons ŕ quoi il ressemble
          jnbe RLE                     ;s'il n'est pas inférieur ou égal ŕ 0C0h
                                       ;alors on doit passer ŕ la décompression

          Single:                      ;sinon on l'affiche tout seul et tout simplement
          mov es:[di],al               ;sur l'écran
          inc di                       ;on passe au prochain octet pour la suite,
          inc si                       ;dans l'écran et dans le fichier
          jmp Fin_Decode               ;on ne fait pas de décompression alors on passe
                                       ;directement ŕ l'octet qui suit

          Rle:                         ;Si jamais le pixel était supérieur ŕ 0C0h
                                       ;alors il faut décompresser
          sub al,0c0h                  ;on soustrait 0C0h pour retrouver le nombre de répet.
          mov bl,ds:[si+1]             ;et dans Bl, on met la couleur du pixel ŕ répeter

          Rle_1:                       ;Nous avons AL=nb de repet / BL=couleur du pixel
          mov es:[di],bl               ;nous affichons ŕ l'écran, un pixel
          dec al                       ;nous descendons le compteur
          inc di                       ;mais passons au pixel suivant sur l'écran
          cmp al,0                     ;on regarde si on a tout répeté
          jnz Rle_1                    ;tant qu'on a pas fini, on continue
          add si,2                     ;ne pas oublier de passer ŕ la position dans le fichier
                                       ;qui se trouve ŕ 2 octets de l'actuelle...

          Fin_Decode:                  ;on atterit ŕ FIN_DECODE
          cmp di,64000                 ;qui regarde si on s'est fait tout l'écran
          jna Affiche_2                ;si on a pas fini, alors on recommence avec
                                       ;l'octet qui suit

                ************

          xor ax,ax
          int 16h                      ;autrement on attend que l'utilisateur appuye sur
                                       ;une touche
          mov ax,03
          int 10h                      ;si il a appuyé, on remet le mode texte
          ret                          ;et on retourne chez Mr.DOS

          SET_COLOR PROC               ;al=index ah=rouge bl=vert bh=bleu
          pusha
          mov dx,3c8h
          out dx,al        ; index
          mov dx,3c9h
          mov al,ah
          out dx,al        ; Rouge
          mov al,bl
          out dx,al        ; Vert
          mov al,bh
          out dx,al        ; Bleu

          popa
          ret
          SET_COLOR ENDP

          Debut_Image:                 ;ce label sert ŕ situer le début de l'image

          include gill.db              ;ici, c'est le nom du fichier PCX converti en données
                                       ;ASM

          Fin_Image:                   ;ce label lŕ, pour situer la fin de l'image

Améliorions un peu, avec un effet de FADE-OUT

Vous avez sűrement déjŕ vu, un effet de FADE-OUT. L'image s'assombrit pour
disparaître dans un écran noir comme si la lumičre s'éteignait
progressivement. C'est un effet trčs simple ŕ faire. Il existe plusieures
méthodes. Certains préfčrent stocker la palette ŕ quelque part.
Nous n'allons pas nous encombrer avec des DB DUP (?)... La méthode que
je vais expliquer, utilise les routines déjŕ présentées dans le chapitre
sur la palette. Nous utiliserons la procédure SET_COLOR pour mettre une
nouvelle couleur et la procédure GET_COLOR pour lire la valeur de la couleur.

Pour assombrir l'image, il suffit de tester chaque composante et de voir
si elle est égale ŕ zéro. Dans ce cas, il ne faut pas descendre la valeur
de la couleur pour ne pas tomber dans des nombres négatifs, il s'agit tout
simplement du noir. Si la composante a une valeur supérieure ŕ 0, alors
nous descendons cette valeur de 1. Aprčs avoir traité, les trois composantes
de la couleur, nous renvoyons la nouvelle couleur modifiée par nos soins.
Il suffit de parcourir entičrement la palette 63 fois (1 fois ŕ chaque frame)
puisque la valeur maximale d'une composante est 63.
Nous ajoutons aussi une procédure de syncro avec le rayon du tube
cathodique pour éviter que tout cela tourne trop vite.

Arrętons la théorie et passons au code pour A86

Vous trouvez ici, juste la routine pour créer le fade-out. Elle se trouve
ŕ la fin du programme principal avant le retour au mode texte et au DOS.

          xor bp,bp                       ;BP nous sert de compteur

          Fade_0:
          mov b[color],0                  ;on met ŕ zéro COLOR qui est l'index en cours de traitement

          Fade_1:
          call GET_COLOR                  ;on va chercher la couleur dans la carte VGA
                                          ;GET_COLOR nous met R,V,B dans les bytes ROUGE, VERT et BLEU

          Dec_Rouge:
          cmp b[rouge],0                  ;on regarde si la composante rouge = 0
          jz dec_vert                     ;si oui, il faut pas descendre sa valeur et on passe ŕ l'autre
                                          ;couleur
          dec b[rouge]                    ;autrement, tranquille, on décrémente

          Dec_vert:                       ;pareil pour le vert
          cmp b[vert],0
          jz dec_bleu
          dec b[vert]

          Dec_bleu:                       ;et de męme pour le bleu
          cmp b[bleu],0
          jz Fin_Dec
          dec b[bleu]

          Fin_Dec:
          mov al,b[color]                 ;AL prend la valeur de l'index
          mov ah,b[rouge]
          mov bl,b[vert]                  ;on place toutes les couleurs dans les registres
          mov bh,b[bleu]
          call SET_COLOR                  ;et on met notre nouvelle couleur

          inc b[color]                    ;on passe ŕ l'index suivant
          cmp b[color],0                  ;on regarde si l'on a fait toute la palette
                                          ;au lieu de regarder si on a color>255, on regarde
          jnz Fade_1                      ;simplement si c'est égal ŕ zéro / 0-255, ça fait le tour

          inc bp                          ;si on a fait toute la palette, alors il faut faire un
                                          ;nouveau tour
          call SYNC                       ;on appelle la sync. pour ralentir
          cmp bp,63                       ;aurait-on atteint la limite maximale d'une composante ?
          jna Fade_0                      ;pas encore alors retour

          GET_COLOR PROC                  ;récupčre la couleur de b[COLOR]
          mov dx, 03C7h
          mov al, color
          out dx, al
          add dx, 2
          xor bh, bh
          in  al, dx
          mov bl, al                      ;bl=rouge
          in  al, dx
          mov ah, al                      ;ah=vert
          in  al, dx
          mov dx, bx                      ;al=bleu   ok !!!
          mov rouge,bl
          mov vert,ah                     ;on place le tout dans les bytes
          mov bleu,al
          ret
          GET_COLOR ENDP

          SYNC PROC
          mov dx, 3DAh
          retrace1:
          in al, dx
          test al, 8
          jz retrace1                     ;attente du retour du faisceau lumineux.
          retrace2:
          in al, dx
          test al, 8
          jnz retrace2
          ret
          SYNC ENDP

### Chapitre 10 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


Les modes texte

TUTORIAL ASSEMBLEUR - chapitre 11 - Les modes texte

Introduction - Les modes texte

Faisons une petite trčve avec les modes graphiques du VGA pour nous
intéresser de plus prčs aux modes texte qui sont encore
trFs utilisés, surtout sous DOS. Ils sont plus rapides que les modes
graphiques et permettent l'utilisation de plusieurs pages.
Certains modes graphiques comme le Mode-X, l'autorisent, c'est plus
esthétique mais c'est aussi plus lent. La résolution des
modes texte est moins grande mais des effets tout a fait satisfaisant
peuvent Otre programmés avec un peu de patience en mode
texte.

Comment mettre en place ces modes ?

Nous n'allons pas voir les modes non-standards dit "Vesa" car ils demandent
une programmation un peu plus compliquée. Par contre, le BIOS nous propose
quelques modes trFs intéressants et parfaitement suffisants pour faire
de petits effets. En voici quelques uns :

          00 = 40x25 avec 16 couleurs
          02 = 80x25 avec 16 niveaux de gris
          03 = 80x25 avec 16 couleurs

Le mode 03 est le plus courant, c'est celui du DOS. Pour pouvoir mettre en
place un des ces modes, vous devez appeler l'interruption 10h :

          mov ax,numero_mode
          int 10h

Pour le mode 80x25 - 16 coul :

          mov ax,03
          int 10h

A partir de ce moment, nous pouvons afficher du texte :

          mov ax,03
          int 10h
          lea dx,message        ;on met dans DX, l'offset de "message"
          mov ah,09h
          int 21h
          ret
          message db 'éexte - éexte$'

Cela aura pour effet d'afficher le texte et de revenir au DOS. Cependant,
vous ne pouvez pas utiliser de couleurs. Il y a deux
possibilités. Soit on utilise des fonctions de l'interruption 010h ou
alors, on travaille avec la mémoire texte. C'est la deuxiFme
possibilité que nous allons voir.

Utilisation de la mémoire texte

Nous travaillons en mode 80x25 ce qui fait 2000 caractFres.
Pour pouvoir désigner la couleur, nous utiliserons 2000 autres
octets. Une page texte fait donc 4000 octets. La mémoire du texte se
trouve a l'emplacement    0B800h:0000  tandis que la
mémoire vidéo se trouve a 0A000h:0000. Nous allons mettre dans ES,
la valeur du segment, ici 0B800h et se placer au
premier caractFre.

          mov ax,03
          int 10h
          push 0b800h
          pop es
          xor di,di
          ret

Nous avons maintenant dans ES:DI, l'adresse de la mémoire texte.
Un caractFre, dans la mémoire texte, est décomposé en deux parties :

          |caractFre|couleur|
             AL        AH

Un caractFre est donc un Word. Nous platons le caractFre "E" avec la couleur
jaune dans la mémoire texte (ES:DI pointe sur la mémoire texte) :

          mov al,'E'         ; char ascii  dans al  manip. courante
          mov ah,14          ; color fond et texte
          mov es:[di],ax

N'oubliez pas que pour afficher le caractFre suivant, il faut
incrémenter DI de 2. Ici, A86 et les autres assembleurs, nous
permettent de mettre le caractFre entre des ' '. Cela indique a l'assembleur
que nous voulons le code du caractFre entre
apostrophes. Vous pouvez maintenant modifier la couleur de chaque caractFre
qui se trouve a l'écran...

Le flipping des pages

La particularité du mode texte est de permettre le page-flipping.
Il s'agit d'une techique qui consiste a écrire dans une page qui
n'est pas visible pendant que l'on affiche une page déja calculée.
Ensuite, on inverse l'ordre des pages. Cela évite les effets de
scintillements et de coupures. La page n'est affichée que lorsqu'elle
est entiFrement calculée. Cela n'est pas possible en mode
graphique 13h (on peut le faire mais c'est plus compliqué).
L'autre possibilité pour faire du page-flipping en mode graphique est
l'utilisation du mode-x (ou le vesa 2.0).
Donc, le mode texte nous permet le page-flipping. La premiFre page se
trouve a l'offset 0 et la seconde a l'offset 4096 et non
4000. L'offset de la seconde n'est pas a 4000 car on ne peut travailler
que par incrément de 512... Pour afficher un caractFre
sur la seconde page qui n'est pas visible au début :

          mov ah,03eh          ; color fond 03 (bleu)  et char eh (jaune) jmm
          mov al,'E'           ; char ou code ascii                       jmm
          mov es:[di+4096],ax

Voila, nous plaçons dans DI+4096, le nouveau caractFre. Maintenant, nous
allons utiliser un registre de la carte VGA pour faire le flipping.
Nous allons indiquer a l'index 03D4h (CRéC), registre 0c4h qui s'appelle
"Linear Starting Address High", le nouvel emplacement de départ de la
mémoire texte (voir chapitre sur la vidéo pour savoir ce qu'est le CRéC).

          FLIP1 PROC
          mov dx, 3D4h
          mov al, 0Ch
          mov ah, 0             ;emplacement 0*512=0  - c'est la premiFre page
          out dx, ax
          ret
          FLIP1 ENDP

          FLIP2 PROC
          mov dx, 3D4h
          mov al, 0Ch
          mov ah, 8             ;emplacement 8*512=4096 - c'est la deuxiFme page
          out dx, ax
          ret
          FLIP2 ENDP

En appellant FLIP1, on affiche la page1 et en appellant FLIP2, on affiche
la page 2. Donc le page-flipping consiste a appeller FLIP1 pendant que
l'on écrit les caractFres dans la page 2 et dFs qu'on a fini, on appelle
FLIP2 et on écrit dans la page 1. On continue comme ta...

D'autres modes et techniques

Vous avez svrement déja vu le mode texte 80x50, les caractFres sont carrés.
Voici la procédure pour l'initialiser :

          mov ax,03h
          int 10h
          xor ax,1111h
          int 10h

On l'utilise comme le mode 80x25. Si vous voulez enlever le curseur qui
peut parfois Otre gOnant, utilisez cela:

          ENLEVE:
          mov ax,0100h
          mov cx,02607h
          int 10h

Pour l'afficher a nouveau, soit vous réinitialisez le mode texte ou
vous faetes :

          AFFICHE:
          mov ax,0100h
          mov cx,0506h
          int 10h

### Chapitre 11 - dake / c a l o d o x ###
### http://www.space.ch/scene/calodox ###


ASM Mode Reel

Introduction du cours et sommaire Vu dans ... https://openclassrooms.com/

Vous tombez bien ! Ce cours est destiné à tout le monde, y compris ceux qui débutent et qui n'ont jamais programmé. On a beau dire que l'assembleur est un langage dur, son véritable obstacle est en fait le manque de cours clairs et progressifs, pour débutants. Je pense que j'aurais eu bien du mal à apprendre l'assembleur x86 si je n'avais pas fait de l'assembleur z80 auparavant. C'est pour cela que j'ai fait ce tutoriel, afin d'aider tous ceux désireux d'apprendre ce langage, même s'ils n'ont jamais programmé.

L'assembleur est un langage très important pour avoir une vraie proximité avec l'électronique, contrôler de manière précise le matériel et comprendre le fonctionnement réel de l'informatique. Il est d'ailleurs également possible de programmer d'excellents jeux avec, comme par exemple :

Roller Coaster Tycoon 2, jeu de gestion de parcs d'attraction programmé 99% en assembleur par une seule et même personne, son collègue ayant fait les graphismes

Roller Coaster Tycoon 2, jeu de gestion de parcs d'attraction programmé 99% en assembleur par une seule et même personne, son collègue ayant fait les graphismes

Vous voulez tenter l'aventure en vous projetant dans une nouvelle dimension que celle de l'assembleur ? C'est par ici...

·

Partie 1 - Les bases

1. 1. Introduction / Installation

2. 2. Votre premier programme

3. 3. Manipulation et test des nombres

4. 4. Électronique : la mémoire


ASM Mode Reel

Introduction / Installation Vu dans ... https://openclassrooms.com/

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Avant de se jeter littéralement à plat ventre sur les programmes, il faut bien commencer par... le début.
Comprendre ce que l'on va faire, étudier quelques préliminaires et installer un logiciel d'assemblage.

Mis à part deux paragraphes où cela est expressément indiqué, il est important de lire ce chapitre avant de passer à la suite. Ce n'est pas qu'une introduction et le cours commence dès ici.

Les notions tournant autour de l'électronique sont volontairement très simplifiées car tel n'est pas le but de ce cours.

L'assembleur, c'est quoi ?Introduction au binaire

Alors tout d'abord, je tiens à vous prévenir que votre ordinateur ne comprend pas le français (je préviens au cas où). Mais que comprend-il, dans ce cas ? Le langage machine. J'en conviens, cela ne nous avance guère, si ce n'est que nous gagnons un mot de vocabulaire. Encore faut-il savoir ce qu'il signifie, et ça, c'est une autre paire de manches.

En fait, il n'est rien de plus qu'un amas de 0 et 1. Ce n'est pas plus compliqué que ça ? Non, si vous écrivez "01011010011", c'est déjà du langage machine que votre ordinateur comprend. C'est fou, non ? C'est bien gentil, mais comment comprend-il ces 0 et 1 ? Bonne question, et c'est ce à quoi on va bientôt s'affairer.

Mais avant tout, j'ai un révélation à vous faire : un ordinateur est, pour faire simple, un énorme paquet d'électronique dans lequel chaque fil n'a que deux choix :

soit il véhicule du courant ;

soit il n'en véhicule pas.

Alors il a été décidé que si le courant passe, c'est l'état 1, et que s'il ne passe pas, c'est l'état 0. Ainsi, le langage de l'ordinateur n'est pas si difficile : des fils électriques dans lesquels le courant passe ou non.

Signification des 0 et 1

Vous savez tout : le langage machine est un langage qui s'exprime avec des 0 et 1 (courant qui passe ou non dans un fil électrique), ce qui se nomme le binaire. C'est un choix de vie, les humains ont quand à eux opté pour des mots ; par exemple, ceux qui constituent cette phrase. Apprenons donc cette fabuleuse langue des ordinateurs !

Binaire : forme de codage des données utilisant les chiffres 0 et 1. Nous verrons dans la section suivante que c'est une base 2, tout comme le décimal est une base 10.

Bit (BInary digiT), qui donnerait en français chib (CHIffre Binaire) : il s'agit d'un chiffre composant la base binaire, c'est-à-dire un 0 ou un 1.

11000111

Il est temps de passer à l'interprétation des ces 0 et 1 (rappel : le binaire). Figurez-vous que cette interprétation change en fonction de la machine : tout comme il y a plusieurs milliers de langues sur Terre, il existe de nombreuses langues binaires, c'est-à-dire de langages machine différents. Votre ordinateur ne parle pas la même langue que ses ancêtres, mais ne parle pas non plus la même langue que par exemple votre téléphone ! Dans ce cours, nous apprendrons l'assembleur x86, présent sur la plupart des ordinateurs. D'autres assembleurs répandus sont l'ARM, le MIPS...

Vous le verrez par la suite, l'assembleur est en fait une interprétation du binaire avec des mots. L'assembleur est exactement la même chose que le langage machine mais écrit avec des caractères habituels plutôt que des 0 et 1.

Les neurones d'un ordinateur

Cette section n'est pas nécessaire pour la suite. Je vous conseille tout de même d'essayer de la lire mais ne vous en faites pas si vous ne comprenez pas.

Mais au fait, comment construit-on un langage machine ? Grâce à des portes logiques. Ce sont des composants électroniques qui admettent une ou plusieurs entrées, et une ou plusieurs sorties. Pour commencer, nous allons voir les circuits logiques combinatoires : à tout instant, la sortie dépend uniquement des entrées. Nous verrons plus tard qu'il est possible de faire des circuits pour lesquels ce n'est pas le cas, mais pour l'instant, contentons-nous de cela.

Or donc, chaque composant reçoit en entrée des signaux électriques à travers des fils : à chaque fil correspond une entrée et chaque entrée est un 0 ou un 1.

Le composant reçoit ces informations et en renvoie une en retour : un 0 ou un 1 (nous ne nous intéressons pour l'instant qu'aux composants à une sortie).

Voici de ces composants :

Nom | Représentation | Explications
AND (ET) | AND AND / ET | Renvoie du courant si et seulement si les deux entrées en envoient.
OR (OU) | OR OR / OU | Renvoie du courant si et seulement si au moins une des deux entrée en envoie.
NOT (NON) | NOT NOT / NON | Renvoie du courant si et seulement si l'entrée n'en envoie pas.
XOR (OU BIEN) | XOR XOR / OU BIEN | Renvoie du courant si et seulement si une seule des deux entrées en envoie.

Pour comprendre ce tableau, procédons par comparaison : imaginez par exemple que vous souhaitiez cuisiner un gâteau au chocolat (dont la recette miracle fait qu'un peu de farine et de chocolat suffisent). Le composant "AND" ("ET") peut être assimilé à un processus qui va vous dire si vous pouvez faire votre gâteau au chocolat ou non. En entrée :

Y a-t-il de la farine ? Oui = 1, non = 0.

Y a-t-il du chocolat ? Oui = 1, non = 0.

En sortie :

Allez-vous pouvoir préparer le gâteau ? Oui = 1, non = 0.

Bien sûr, on a besoin de tous les ingrédients pour préparer le gâteau. Il faut qu'il y ait de la farine ET du chocolat. C'est donc le composant "AND" qui répond à nos attentes.

Si du courant passe dans les deux entrées, il passera également dans la sortie. Dans tous les autres cas, le courant ne passera pas.

Autre exemple : faut-il allumer le four ? Supposons que la réponse soit oui si un gâteau au chocolat OU un gâteau au yaourt va (ou vont) être préparé(s). Cette fois, le composant qui apporte une solution au problème est le composant "OR".

Maintenant, combinons les deux circuits, avec un autre en plus, afin de savoir s'il faut lancer le four, en sachant s'il y a de la farine, du chocolat et/ou des yaourts (en supposant que les ingrédients du gâteau au yaourt sont la farine et les yaourts ) :

N'hésitez pas à examiner ce circuit jusqu'à bien en comprendre le fonctionnement.

Maintenant, accrochez-vous bien : nous avons déjà le niveau pour inventer notre propre langage machine ! Facile, hein ?

Pardon ?

Rassurez-vous, je vais bien vous guider et vous allez comprendre ce que c'est, le langage machine. Déjà, nous savons que c'est en binaire. Et avec ça, nous allons pouvoir former des instructions, mot qui sera défini un peu plus bas.

Nous allons créer un circuit électronique qui, en fonction de ce qu'on met en entrée, allume ou éteint une ampoule. Disons qu'en entrée, on demande 4 bits (rappel, c'est-à-dire 4 chiffres valant chacun 0 ou 1). En sortie, il y a deux fils qui commandent l'ampoule : un qui lui demande de s'éteindre, un qui lui demande de s'allumer.

En fait, ce qu'on a en entrée, c'est une instruction. Elle va dire "fais ci, fais ça". Et le circuit se contente d'obéir. Il traite l'information et commande l'ampoule. Inventons deux instructions :

"1011" : allumer l'ampoule

"1010" : éteindre l'ampoule

Voici le circuit, car je sens que vous ne comprenez rien de rien :

Reprenons : notre circuit admet en entrée une instruction de 4 bits. On peut numéroter ces bits de 0 à 3. Si on entre dans le circuit l'instruction binaire "1011" :

bit n°3 = 1

bit n°2 = 0

bit n°1 = 1

bit n°0 = 1

Maintenant, regardez ce qui se passe si on entre cela dans notre circuit. N'hésitez pas à jeter un œil sur le tableau des composants. Alors ? Du courant est envoyé vers ON (mais non vers OFF). Voici un schéma qui le montre (les fils rouges sont les fils où du courant se propage) :

Et si on met en entrée l'autre instruction de notre invention, "1010", du courant est envoyé vers OFF mais non vers ON :

Est-ce révolutionnaire ? Pas vraiment, nous aurions tout simplement pu admettre en entrée une instruction d'un seul bit :

Mais l'avantage de notre circuit, c'est qu'on peut fabriquer d'autres instructions ! Par exemple :

allumer la machine à café

éteindre la machine à café

allumer la télé

ne rien faire

Ne rien faire ? Mais comment ? Et à quoi cela rime-t-il?

Cette possibilité existe déjà dans notre circuit. Par exemple, essayez d'envoyer "0000" dans le circuit : il ne se passe rien, rien n'est retransmis vers l'interrupteur. Et quel en est l'intérêt ? Je ne sais pas pour vous, mais personnellement, je n'ai pas forcément envie que mon ampoule ne cesse de s'allumer et s'éteindre, sinon, elle aura vite fait de claquer. J'ai parfois envie qu'elle reste dans son état actuel. Eh bien voilà à quoi ça sert.

Maintenant, nous allons créer un programme, c'est-à-dire une suite d'instructions, afin de faire une guirlande pour votre sapin de Noël :

1011

0000

1010

1011

0000

0000

1010

0000

Nous supposerons qu'il y a une machine qui se charge d'envoyer une à une les instructions vers notre circuit électronique, mettons une instruction par seconde. Que se passe-t-il ? Parfois, on allume les loupiotes, parfois on les éteint, parfois, on les laisse dans leur état. Lorsqu'on met "0000", ça ne fait rien de plus qu'un effet de temporisation (timer en anglais) : les loupiotes restent plus longtemps dans leur état actuel.

...

Contrôle surprise ! Ajoutez sur le circuit électronique l'instruction "1001" permettant d'allumer la machine à café. Si vous n'avez pas compris comment on fait : "1001" est synonyme de "bit n°3 ET NON(bit n°2) ET NON(bit n°1) ET bit n°0". Pour ceux que ça intéresse, en respectant les conventions, ça donne ça : b3⋅b2¯⋅b1¯⋅b0 . Les connaisseurs me diront que ce n'est pas simplifié. Je m'en contrefiche, ce n'est pas le but de cette sous-partie.

Et ensuite, il suffit d'appliquer les composants "AND", "NOT"...

Solution :

On pourrait très bien optimiser le circuit, mais ce n'est pas ici notre but. Le but est de comprendre comment une instruction est décodée : des 0 et 1 entrent, ils passent par des portes logiques, et, au final, le courant sort à d'autres endroits pour produire des actions.

Essayez d'entrer "1001" dans ce circuit pour vérifier que cette instruction lance la machine à café.

Instruction : ordre que l'on donne par exemple à un circuit et qui doit être exécuté. Dans notre exemple, "1011" pour allumer l'ampoule est une instruction, "0000" pour ne rien faire en est une autre.

Quartet : groupe de 4 bits. Notre circuit admet donc comme instructions des quartets.

Bit de poids fort : le bit le plus à gauche, ici, le bit 3.

Bit de poids faible : le bit le plus à droite, donc le bit 0.

Bon, c'est bien joli, tout ça, mais notre programme n'est pas très lisible pour un humain... choisissons donc de donner un petit surnom à chacune de nos instructions. Par exemple, "ON" pour "1011", "OFF" pour "1010", "NOP" pour "0000" et "CAFE" pour "1001". Notre programme devient :

ON

NOP

OFF

ON

NOP

NOP

OFF

NOP

CAFE

(Je me suis permis de faire servir le café à la fin de l'animation.)

Et c'est ainsi qui naquit l'assembleur ! L'assembleur, c'est tout bêtement une traduction du binaire en mots-clés plus faciles à comprendre et plus pratiques. Eh bien c'est dans un tel langage que nous allons apprendre à programmer, mais bien sûr dans un assembleur bien plus complexe et varié. Tenez, voici d'ailleurs le premier programme que nous réaliserons, dans le prochain chapitre :

org 0x0100

; ceci est un programme affichant

; simplement "Hello World"

mov dx, texte

mov ah, 0x09

int 0x21 ; afficher le hello world

ret ; fin du programme

texte: db 'Hello, World !!', 10, 13, '$'

La notion de base

Dans la vie de tous les jours, on compte en base 10 (la base décimale), c'est-à-dire que nos nombres comportent 10 chiffres allant de 0 à 9. Pourquoi ? Parce que nous avons 10 doigts et que nos nombre héritent de la méthode ancestrale qui est de compter avec les doigts. (Quoique vous savez peut-être que les Mayas comptent en base 20, peut-être qu'ils comptaient aussi avec leurs orteils. :p)

Eh bien nous allons ici apprendre 2 autres bases :

La base 2 (binaire). Un interrupteur pour ampoule n'a en effet que deux doigts : marche et arrêt. Nous l'avons abordée dans la section électronique. Les nombres écrits en binaire se composent en fait des chiffres 0 et 1.

La base 16 (hexadécimal). Les nombres écrits en hexadécimal se composent des chiffres 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A (=10), B (=11), C (=12), D (=13), E (=14) et F (=15).

Voici des exemples de nombres :

754 = nombre écrit en décimal
101100 = nombre écrit en binaire
5E3FA = nombre écrit en hexadécimal

Pour repérer chaque chiffre dans le nombre, nous numéroterons de droite à gauche, en commençant par 0 :

5 | 4 | 3 | 2 | 1 | 0
- | - | - | 7 | 5 | 4
1 | 0 | 1 | 1 | 0 | 0
- | 5 | E | 3 | F | A

Calculons la valeur de chaque nombre en l'écrivant sous forme décimale. Il suffit de faire la somme des [image] [image] [image] [image] [image] [image] [image] [image] [image] [image] [image] [image] [image] chiffre×basen ﻿, avec [image] n le numéro correspondant à [image] [image] < [image] [image] [image] [image] [image] chiffre . Exemples :

Décimal (base 10) : 1647=1×103+6×102+4×101+7×100=1647 (on retrouve bien le bon résultat)
Binaire (base 2) : 101101=1×25+0×24+1×23+1×22+0×21+1×20=45 ﻿
Hexadécimal (base 16) : 5C8=5×162+12×161+8×160=1280+192+8=1480

Et voilà le travail ! Nous savons maintenant convertir un nombre de n'importe quelle base vers la base décimale. Il ne reste plus qu'à savoir aller dans l'autre sens : de la base décimale vers n'importe quelle autre base. Pour cela, on effectue des divisions euclidiennes successives par la base voulue : le premier reste va donner le chiffre le plus à droite, le deuxième reste l'avant-dernier chiffre etc. jusqu'à ce qu'on trouve un résultat nul : inutile alors de poursuivre car toutes les divisions euclidiennes donneront 0 ! Un exemple ne fera pas de mal. Conversion du décimal 1460 en hexadécimal :

Si un point vous échappe, n'hésitez pas à relire. Je vous invite à tester par vous-mêmes pour bien comprendre. Si l'on veut convertir un nombre binaire en décimal, on procède de la même manière en effectuant cette fois-ci des divisions euclidiennes par 2. N'oubliez pas d'inverser le sens des restes pour avoir le nombre final.

Il existe une autre méthode pour faire des conversions entre une base et une autre ; la première fois, c'est assez sportif, mais avec un peu d'habitude, elle se révèle très efficace : utiliser la calculatrice de votre système d'exploitation (Windows, Linux, Mac, ...). Pour Windows et Linux, elle se trouve dans les "accessoires". Si l'option se présente, faire Edit (ou Fichier) puis mettre en mode programmeur. Sélectionner une base parmi celles proposées, entrer un nombre, puis sélectionner une autre base.

Voici un petit encadré avec du vocabulaire à connaître :

Binaire : base 2.

Hexadécimal : base 16.

Quartet : groupe de 4 bits.

Octet : groupe de 8 bits, soit 2 quartets.

Bit de poids fort : le bit qui porte le plus grand indice [image] n ﻿ (par exemple, c'est 7 dans le cas d'un octet).

Bit de poids faible : le bit qui porte le plus petit indice [image] n ﻿, c'est-à-dire 0.

En notation hexadécimale, un chiffre donne 16 possibilités. De plus, 4 bits donnent également 16 possibilités. Pour alléger les notations, on peut donc noter chaque quartet par un chiffre hexadécimal. Chaque octet peut donc se noter par 2 chiffres hexadécimaux. Correspondance décimal <-> hexadécimal <-> binaire entre 0 et 15 :

Décimal | Hexadécimal | Binaire
0 | 0 | 0000
1 | 1 | 0001
2 | 2 | 0010
3 | 3 | 0011
4 | 4 | 0100
5 | 5 | 0101
6 | 6 | 0110
7 | 7 | 0111
8 | 8 | 1000
9 | 9 | 1001
10 | A | 1010
11 | B | 1011
12 | C | 1100
13 | D | 1101
14 | E | 1110
15 | F | 1111

Très vite, vous connaîtrez ce tableau sur le bout des doigts, et il vous sera facile de faire des conversions hexadécimal <-> binaire de tête. Par exemple, convertir 4D73 en binaire.

4 -> 0100
D -> 1101
7 -> 0111
3 -> 0011

Au final : 4D73 -> 01001101 01110011

Idem pour la conversion dans l'autre sens.

Tenez, vous êtes maintenant aptes à comprendre une vieille blague de programmeurs : "Dans la vie, il y a 10 types de gens : ceux qui comprennent le binaire et ceux qui ne le comprennent pas."

Il faut comprendre 10 comme un nombre binaire. Ça donne donc "il y a 2 types de gens". :D Ben quoi, ce n'est pas drôle ? 0 :euh:

Ce par quoi l'on va commencer

Je vous le dis tout de suite, nous ne commencerons pas par un jeu 3D. Nous nous contenterons de débuter par le plus simple : une console DOS exécutant des programmes en ".COM" (et cela ne représente pas un des domaines comme .org ou .fr).

Pour ceux qui ne savent pas ce qu'est une console, ce n'est pas une plate-forme pour jeux vidéos, mais c'est plutôt les débuts de l'informatique. Voici une console sous Windows :

Invite de commandes de Windows XP Invite de commandes de Windows XP

Ne dites pas que c'est moche, vous risqueriez de vous faire des ennemis dans le domaine de l'informatique ! À moins que vous ne préfériez une console sous Linux :

Invite de commandes de Linux Invite de commandes de Linux

Vous allez me dire que ce n'est vraiment pas attrayant ce que je vous propose là et que vous avez déjà envie de partir faire autre chose plutôt que de travailler sur ce machin noir et blanc où il n'y a que des écritures. Bon, d'accord, il est vrai que ça ne paraît pas très amusant, mais croyez-moi, c'est ce qu'il y a de mieux pour débuter. Nous réussirons tout de même à faire des choses tout à fait sympathiques et dès que nous le pourrons, nous passerons aux graphismes !

Et puis en plus, ça fait classe devant des potes de travailler avec une console. :soleil:

Notion d'assemblage et d'exécution

Matériel

Lorsque vous écrirez un programme assembleur, ce sera du texte, dans un fichier texte. Mais le processeur ne pourra pas directement comprendre ce texte ; il faudra donc le traduire en langage machine. Nous assemblerons notre fichier. Le logiciel lira le fichier ligne par ligne et, lorsqu'il tombera sur "OFF" dans notre exemple de tout à l'heure, il le traduira en "1010" : ça, le processeur comprendra. Bien sûr, l'assembleur x86 que nous apprendrons est un langage plus complexe que celui que nous avons inventé.

Donc finalement : on a un fichier texte, on l'assemble avec un assembleur, et ça donne un exécutable, c'est-à-dire un programme directement lisible pour le processeur, constitué de 0 et 1.

Ah, j'oubliais : certes, l'assembleur a été inventé. Mais bien sûr, il a fallu que chacun invente sa propre norme, sa propre manière de traduire le langage machine. Dans notre exemple, on aurait pu choisir "EL" comme "Enable Light" à la place de "ON", "DL" comme "Disable Light", conserver "NOP" pour "ne rien faire", et choisir "COFFEE" à la place de "CAFE" :

1ère notation | 2nde notation | Langage machine
ON NOP OFF ON NOP NOP OFF NOP CAFE | EL NOP DL EL NOP NOP DL NOP COFFEE | 1011 0000 1010 1011 0000 0000 1010 0000 1001

Il y a de nombreuses ressemblances entre tous les assembleurs mais aussi de nombreuses différences. Mais, en fin de compte, ils traduisent un même langage machine d'un même processeur. Mon choix s'est fait pour NASM (Netwide ASseMbler), téléchargeable ici : http://www.nasm.us/. Installez-le, nous en aurons besoin pour la suite.

Ensuite, vos programmes assembleur, vous pouvez les saisir avec le logiciel que vous voulez du moment qu'il génère un fichier texte. Sous Windows, l'éditeur par défaut est le bloc-notes mais il très rudimentaire. Pour pallier à ses défauts, il existe un logiciel bien meilleur, Notepad++, qui dispose d'une coloration syntaxique et de bien d'autres outils... Vous pouvez également utiliser Sublime Text, ou n'importe quel autre éditeur avancé, du moment qu'il vous convient.

La mémoire

Dans votre ordinateur, il y a plusieurs types de mémoire : la mémoire vive (ou RAM = Random Access Memory), le disque dur, la mémoire morte (ou ROM = Read Only Memory) etc. La mémoire vive a besoin de courant pour conserver les données. Une panne de courant et pof ! tout se remet à 0 (par exemple, vous perdez un compte-rendu sur lequel vous avez passé des heures et que vous n'avez pas sauvegardé). Mais son avantage, c'est qu'elle est plus rapide, plus accessible... et en plus, seuls les programmes qui y sont chargés peuvent être exécutés !

Lorsque vous cliquez sur un programme pour l'exécuter (au hasard Firefox), il est doit donc d'abord être chargé dans la mémoire vive et ensuite, il peut être lancé. Voyons à quoi ressemble cette RAM :

L'association des emplacements mémoire à une adresse

La notation "0x" signifie qu'il s'agit d'un nombre hexadécimal. Comme vous pouvez le voir, dans la RAM, il y a plein de petits octets... On peut représenter la valeur de chaque octet par un nombre hexadécimal à 2 chiffres. Mais tous ces octets ne sont pas disposés n'importe comment, sinon, on ne s'y retrouverait plus. Alors les informaticiens ont décidé de faire comme à l'école primaire : on se tient en rang deux par deux. Chaque adresse donne sa main à un contenu et vice-versa.

Bien sûr, on fait ça dans l'ordre des adresses : 0, 1, 2, 3 etc. Et c'est bien pratique, car on peut ainsi facilement retrouver un contenu, il suffit d'avoir son adresse.

Où en étais-je ? À Firefox. Donc on clique dessus, et hop, chacun de ses octets est chargé dans la mémoire RAM, à un endroit où le système d'exploitation (OS) a vu qu'il y avait de la mémoire libre :

Chargement de Firefox dans la mémoire vive Chargement de Firefox dans la mémoire vive

L'exécution

La compréhension de cette section n'est pas nécessaire pour la suite.

Ensuite est chargée dans un "curseur" de son petit nom EIP l'adresse à partir de laquelle ont été chargées les instructions. En fait, EIP est un registre (nous verrons ce qu'est un registre plus tard) qui va contenir un nombre : une adresse. Il a pour but de pointer sur l'adresse courante, là où en est l'exécution du programme.

Initialisation de EIP Initialisation de EIP

À l'impulsion suivante, le contenu de EIP est envoyé vers le bus d'adresse de la mémoire. Quand je parle d'impulsion, il s'agit en fait de cycle. Savez-vous ce que c'est qu'une galère ? Il y a un batteur au fond qui bat le rythme sur son tambour et les rameurs suivent le rythme. L'ordinateur, c'est pareil : une horloge envoie des impulsions électriques à un certain rythme dans le processeur. Et à chaque impulsion, il se passe quelque-chose. Chacune de ces impulsions se nomme un cycle.

En fait, une impulsion électrique est caractérisée par un moment où un courant est établi dans les circuits et produit donc un résultat après passage dans les portes logiques. Diverses actions peuvent en découler et ici, en l'occurrence, la copie de la valeur de EIP dans le "bus d'adresse de la mémoire".

Chargement de EIP dans le bus d'adresse de la mémoire Chargement de EIP dans le bus d'adresse de la mémoire

À l'impulsion suivante, les données de la mémoire à l'adresse indiquée sont copiées dans le registre I.

Chargement dans I de la case mémoire dont l'adresse est donnée par le bus d'adresse de la mémoire Chargement dans I de la valeur de la case mémoire dont l'adresse est donnée par le bus d'adresse de la mémoire

À l'impulsion suivante, les différents bits de I son envoyés vers l'ALU qui en fait traite l'instruction. Nous en parlerons plus tard. Et enfin, EIP est incrémenté (sa valeur augmente de 1).

Incrémentation de EIP Incrémentation de EIP

Et puis on recommence.

Pour résumer :

la valeur de EIP (soit l'adresse de l'instruction) est chargée vers le bus d'adresse de la mémoire ;

le contenu d'adresse indiquée par ce bus est copié dans I ;

les bits de I sont envoyés vers l'ALU, qui se charge se faire des opérations, calculs etc. ;

EIP est incrémenté.

Le problème, c'est qu'il faut beaucoup de cycles pour décoder une instruction :

Le processus d'exécution d'une instruction en plusieurs cycles Le processus d'exécution d'une instruction en plusieurs cycles

Il n'a pas fallu longtemps pour remarquer que certaines étapes ne faisaient rien pendant que les autres bossaient. C'est un peu comme si il n'y avait qu'une partie des galériens qui ramait en même temps. Nous, pendant que l'ALU turbine, EIP ne fait rien, idem pour d'autres intermédiaires.

Pour accélérer la vitesse, on a alors dit à tout le monde "au boulot, et que ça saute !". Pendant que l'ALU bosse, I s'occupe déjà de la prochaine instruction, et EIP se remplit pour la suivante.

Imaginez-vous dans une entreprise de Ford, avec le travail à la chaîne. Disons que :

le premier travailleur fait la carrosserie ;

le deuxième lui ajoute un moteur ;

le dernier décore avec de la peinture.

Eh bien tout le monde travaille ! On s'occupe de plein de voitures en même temps. Par exemple, une fois que les sièges ont été disposés dans la voiture, la machine s'occupe immédiatement de la voiture suivante sans attendre que la précédente soit terminée. De même, l'ordinateur s'occupe de plusieurs instructions en même temps.

Le travail à la chaîne : traitement de plusieurs instructions en un seul cycle Le travail à la chaîne : traitement de plusieurs instructions en un seul cycle

Par conséquent, une instruction met toujours autant de cycles à être exécutée, tout comme une voiture met autant de temps à être fabriquée, mais plusieurs intructions sont traitées simultanément et le rendement est fortement accru.

Nous allons pouvoir commencer et nous attaquer aux bases du langage x86 !

#


ASM Mode Reel

Votre premier programme Vu dans ... https://openclassrooms.com/

Dans de nombreux cours sur l'assembleur, il y a beaucoup de théorie dès le début et on ne commence à coder qu'après beaucoup trop de temps. Résultat : on en a marre et on abandonne. C'est pour cela que j'ai choisi que nous allions, dès ce chapitre, faire notre premier programme, mais en passant tout de même par les notions les plus essentielles.

Il s'agira... d'un "HELLO WORLD !" (affichage de texte dans la console). Magnifique, non ? :ange: Rassurez-vous, ce ne sera qu'un début...

Vous devez avoir lu le premier chapitre qui n'est pas qu'une introduction. Rassurez-vous, sa lecture est rapide et il contient deux parties facultatives.

Les registres

Notion incontournable en assembleur. Les registres, que vous allez découvrir, on les utilise TOUT LE TEMPS. Concrètement, ce sont de toutes petites zones mémoire (quelques dizaines d'octets en faisant l'addition de tous les registres) logées dans le processeur. Dessus, on fait des calculs, des calculs... rien que des calculs ! Eh oui, vous le découvrirez bien assez tôt : votre ordinateur fonctionne avec des calculs. Simples, basiques, mais en les enchaînant, ça donne... un jeu 3D, du traitement de texte, un navigateur, et bien d'autres...

Commençons donc sans tarder la petite histoire des registres. Il était une fois... AX, BX, CX et DX. Pas très poétiques ces inventeurs du x86 ! :p
Ce sont 4 registres parmi d'autres. Ils contiennent chacun 16 bits (2 octets). Une petite présentation avec vos futurs collègues s'impose !

AX : ce registre est nommé l'Accumulateur. C'est le registre le plus utilisé car dessus sont concentrés de nombreux types de calculs possibles à effectuer.

BX : le registre de Base. Il est utilisé comme référence d'accès à des tableaux.

CX : registre de Compteur. Il est utilisé pour les répétitions d'instructions et décalages logiques.

DX : registre de Données. Il sert en fait d'extension à AX.

Chacun de ces registres possède une partie haute et une partie basse : l'octet de poids fort et celui de poids faible. Si vous remplacez le X par H (comme High, Haut en français)), vous désignez l'octet de poids fort, et avec L (Low, bas en français), l'octet de poids faibLe. Une image ne fera pas de mal (les petits carrés bleus représentent des bits) :

Le registre AX en tant que fusion de AH et AL Le registre AX en tant que fusion de AH et AL

Ici, il s'agit du cas de AX, mais cela revient au même avec BX, CX et DX.
On a ainsi les registres 8 bits : AH, AL, BH, BL, CH, CL, DH et DL.

En fait, les "X" dans AX, BX, CX et DX signifient que ce sont des eXtensions de AH, AL, BH, BL, CH, CL, DH et DL (ou plutôt des fusions).

Ces registres se nomment les

Bon, il n'est pas tout de connaître le petit nom de ces gentils messieurs les registres, mais encore faudrait-il apprendre à les manipuler. Nous allons pour cela apprendre une première instruction dont la syntaxe est :

mov {destination}, {source}

Alors en assembleur, la constitution d'une ligne, c'est bien simple : on commence par un mot-clé (ici mov) puis on donne un voire deux paramètres (ici 2). mov indique que l'on va affecter à un registre (ou à une variable, on verra ça plus tard) une valeur. Le premier paramètre est donc un registre, et le deuxième un registre ou un nombre :

mov ax, 53 ; ax contient la valeur 53

mov bx, ax ; bx contient la valeur 53

mov ch, bl ; la partie haute de cx contient la valeur de la basse de BX

Il est possible de terminer une ligne par un commentaire en plaçant un point-virgule. Tout commentaire est ignoré et ne sert que d'aide au programmeur. De même, toute ligne vierge est ignorée mais permet d'aérer le code.

Attention ! Il est interdit de faire un mov entre deux registres d'un nombre différent de bits. Ainsi sont interdits à titre d'exemple :

mov ax, bl

mov ch, bx

Notez qu'en fait de nombre, vous pouvez renseigner en deuxième argument une expression (par exemple 5*45+4), du moment que qu'elle correspond à une valeur immédiate (c'est à dire que la valeur est connue lors de l'assemblage et non de l'exécution).

Comment la machine parvient-elle à ranger un nombre dans un registre, qui n'est qu'une suite de bits ? Dans le chapitre précédent, nous avons vu que les bits étaient des 0 ou 1 et qu'en en alignant, on pouvait obtenir un nombre écrit en binaire. L'humain peut le convertir en décimal pour mieux comprendre ce qu'il vaut, mais les registres contiennent bien des nombres écrits en binaire.

Nous avons déjà vu comment faire la conversion entre écriture binaire et décimale. Seulement, un registre contient un nombre fini de bits ; nous ne pourrons donc y ranger qu'un nombre fini de nombres.

Un registre de 1 octet, soit 8 bits, peut correspondre à 2^8 = 256 combinaisons différentes : pour ceux qui ne connaissent pas cette formule, un bit permet de représenter 2 combinaisons ; 2 bits, 2 fois plus etc. : on multiplie le nombre de possibilités par deux à chaque ajout d'un bit. Avec 8 bits, on a donc 2*2*2*2*2*2*2*2 = 256 combinaisons.

Un registre de 2 octets peut, quant à lui, valoir 2^16 = 65536 choses différentes. On peut donc stocker des nombres entiers :

allant de 0 à 255 ou de -128 à 127, pour les registres 8 bits ;

allant de 0 à 65535 ou de -32768 à 32767, pour les registres 16 bits.

Tout cela est une convention, on aurait pu par exemple choisir les nombres de -15 à 240, ou de 30 à 285... Ce qui est important, c'est le nombre de combinaisons. Leur signification est arbitraire ; les programmeurs la choisissent. Vous le verrez mieux lorsque l'on apprendra à représenter non plus des nombres mais du texte, toujours avec des 0 et 1.

Il y a donc deux types de nombres binaires selon la convention attribuée :

les nombres considérés comme toujours positifs sont dits non signés (0 à 255 ou 0 à 65535) ;

les nombres considérés comme pouvant être positifs ou négatifs sont dits signés (-128 à 127 ou -32768 à 32767).

Voici un schéma pour les valeurs de 1 octet, pour mieux voir la façon dont on assigne une valeur à des bits :

Représentation des nombres signés et non signés de 1 octet Représentation des nombres signés et non signés de 1 octet

Bit de poids fort = le bit le plus à gauche. C'est celui qui correspond à l'indice le plus élevé.

Le tableau montre qu'il faudra faire attention aux bornes : si vous incrémentez ce que vous considérez comme étant un 255 non signé, vous tombez sur... un nombre considéré comme étant 0 ; et si vous incrémentez ce que vous considérez comme étant un 127 signé, vous tombez sur... un nombre considéré comme étant -128.

Nous avons donc appris qu'il y avait des registres à 8 et 16 bits (respectivement 1 et 2 octets). Mais depuis longtemps, les ordinateurs sont passés au 32 bits (et même au 64). Nous disposons ainsi de registres 32 bits.

Ce sont simplement des Extensions des registres généraux 16 bits : AX, BX, CX et DX. Les registres 32 bits correspondants se nomment EAX, EBX, ECX et EDX, avec "E" comme "Étendu" ("Extended", en fait) : ce sont des registres qui étendent AX, BX, CX et DX. Leur partie haute n'est pas directement accessible de manière isolée, et leur partie basse correspond aux registres 16 bits :

L'extension EAX sur 32 bits du registre AX de 16 bits L'extension EAX sur 32 bits du registre AX de 16 bits

L'instruction mov s'utilise exactement de la même façon qu'avec les autres registres. Mais cette fois, leur valeur peut aller de 0 à 2^32-1 = 4 294 967 295. Pas mal, non ? :)

Exemples :

mov eax, 123456789 ; eax contient la valeur 123456789 ==> la valeur de AX (+ AH et AL) est modifiée

mov edx, eax

Les données

Les nombres

Nous savons écrire des instructions en assembleur qui seront traduites en binaire dans le fichier assemblé. Il est également possible de demander d'insérer directement des suites binaires en tant que telles pour les inclure à la suite.

Attention toutefois à ne pas exécuter ces endroits-là car nous ne savons pas à quelles instructions assembleur ils correspondent. Par exemple, si j'insère le nombre 13.027 quelque part dans le programme, cela va donner les octets 00110010 11100011 dans le code machine. Mais à quelle instruction correspond 00110010 11100011 ? Cela ne nous intéresse pas, c'est le nombre correspondant que nous considérons.

Pour insérer un octet de donnée, on utilise la commande "db" (Define Byte) ; pour un mot (= 2 octets), on utilise "dw" (Define Word) ; et enfin pour un double-mot (= 4 octets), on utilise "dd" (Define Double word). Puis on fait suivre cela d'un nombre. Mais il ne doit pas dépasser 255 pour un octet, 65 535 pour un mot, et 4 294 967 295 pour un double-mot.

Exemples :

db 45

db 241

dw 746

dw 45

dd 12000000

dd 0

On peut utiliser la virgule pour définir plusieurs données d'un même format en une seule ligne :

db 45, 241

dw 741, 45

dd 12000000, 0

Mais comme la mémoire est en binaire, pour donner un exécutable, le logiciel qui assemble ne génère que du binaire et doit donc faire des conversions. "db 45" devient "00101101", par exemple. Mais attention ! dw 45 ne va pas donner la même chose, mais "00101101 00000000". Eh oui !

Je veux bien que ça donne deux octets, mais pourquoi "00101101 00000000" et pas "00000000 00101101" ?

Bon, vous ne trouvez pas que c'est insupportable, ces longues chaînes binaires ? Ne pourrait-on pas représenter les octets de façon plus courte ? Si, avec l'hexadécimal. En effet, un quartet binaire (= 4 bits) donne 2^4 = 16 possibilités, et un chiffre hexadécimal aussi, comme vu au chapitre précédent.

Donc on peut représenter chaque quartet par un chiffre hexadécimal au lieu de quatre chiffres binaires. Or, un octet est composé de 2 quartets (2*4 = 8 bits). Finalement, chaque octet peut être représenté par un nombre hexadécimal à 2 chiffres.

Par exemple, "db 45" donne "2D" et "dw 45" donne "2D00". Un peu plus léger, non ?

Reprenons ce code :

db 45, 241

dw 741, 45

Il sera assemblé ainsi :

00101101 11110001

11100101 00000010 00101101 00000000

Ou, écrit en hexadécimal :

2D F1

E5 02 2D 00

N'oubliez pas que les deux octets sont intervertis pour les mots de donnée.

Nous avons vu comment définir un nombre en le donnant sous forme décimale. Mais on peut aussi l'exprimer en différentes bases :

binaire : on fait suivre le nombre d'un "b" comme binaire ;

décimal : on écrit tout simplement le nombre, et on peut aussi le faire suivre d'un "d" comme décimal (facultatif) ;

hexadécimal : on fait suivre le nombre d'un "h" comme hexadécimal ou bien on le fait précéder de "0x".

Exemples :

db 45, 0x7F, 1011b

dw 7E6Bh, 11100101101b, 45d

Attention, une exception pour les nombres hexadécimaux : un nombre ne commence jamais par une lettre. Donc si vous utilisez la convention avec le suffixe "h" et que le nombre commence par un chiffre entre A et F, il faut ajouter un 0.

db E4h ; NON

db 0E4h ; OUI

Sinon, l'assembleur (ici : le logiciel qui assemble) croit que c'est une étiquette, notion que l'on va très bientôt aborder.

Remarque : nous avons appris l'hexadécimal, le binaire et le décimal. Il existe également une autre convention, quasiment plus utilisée, celle de l'octal, comportant les chiffres de 0 à 7. Pour l'utiliser, il faut faire suivre le nombre de "o".

Le texte

Il est également possible d'insérer du texte dans du code NASM. Mais comme l'ordinateur ne comprend que le binaire, il a bien fallu inventer un système de correspondance entre le binaire et le texte. Un système qui à chaque nombre attribue un caractère, par exemple, 0 pour un A, 1 pour un B etc. Et bien sûr, ces systèmes de correspondance, il y en a des centaines de différents. On pourrait même inventer le nôtre.

Avec NASM, la norme automatiquement utilisée est l'ASCII : c'est une norme qui à chaque nombre 8 bits (1 octet) attribue un caractère. Cela permet de représenter jusqu'à 256 caractères. On peut ainsi enregistrer 1 caractère dans un registre 8 bits et deux caractères dans un registre 16 bits. Il suffit de mettre ces caractères entre guillemets ou apostrophes :

db "a"

db "A", 'h'

mov AX, "("

mov BH, 'U'

Maintenant, si l'on veut insérer dans notre code le fameux "Hello World" qui viendra tout à l'heure, ou pourrait procéder comme ceci :

db "H", "e", "l", "l", "o", ",", " ", "W", "o", "r", "l", "d", " ", "!", "!"

Mais les programmeurs n'étaient pas du tout contents d'une écriture aussi lourde. Ils ont donc inventé une écriture plus simple, en se passant de toutes les séparations entre les caractères :

db "Hello, World !!"

Ouf ! C'est mieux comme ça. Comprenez bien que cette notation est une convention ; n'allez surtout pas imaginer que "Hello, World !!" est stocké dans un seul octet parce qu'on utilise la commande db : c'est faux. C'est une suite d'octets, et non un seul octet.

On peut appliquer cela aux registres :

mov ax, ":)" ; AX fait 16 bits, on peut donc mettre deux caractères

J'y pense : surtout, ne croyez pas que ces deux instructions donnent la même chose :

mov bx, "g"

mov bl, "g"

Elles sont aussi différentes que ces deux-là :

mov bx, 0x12

mov bl, 0x12

Dans le premier cas, c'est tout BX qui est affecté, et comme la valeur reçue est inférieur à 0xFF (le plus grand nombre 8 bits), on a BH = 0.

Les étiquettes

Nous sommes donc maintenant capables d'insérer le texte "Hello World" dans notre fichier. Mais si nous voulons l'utiliser, encore faut-il pouvoir le localiser. Par exemple, si nous souhaitons afficher "Bonjour Mamy !", il faut bien localiser ce texte pour l'utiliser. Comme nous l'avons vu, lorsque votre programme est assemblé, il donne une suite d'octets. Avant de lancer le programme, l'OS prend soin de mettre ce programme dans la mémoire vive (ou mémoire RAM).

Or, à chaque case mémoire, il fait correspondre une adresse, qui est en fait un nombre. Comme ça, si on connaît l'adresse d'une donnée, on peut utiliser/modifier le contenu correspondant. Encore faut-il la connaître, cette adresse. À notre niveau, nous considérerons que notre programme est toujours stocké à partir de l'adresse 0x0100 (ce qui est un nombre). De plus, le logiciel d'assemblage connaît la place de chaque instruction une fois assemblée. De par une petite addition, il en déduit donc l'adresse de chaque ligne du programme.

Chargement en mémoire vive d'un programme .com Chargement en mémoire vive d'un programme .com

Pour pouvoir utiliser cette possibilité, il faut d'abord dire au logiciel d'assemblage où sera stocké le programme :

org 0x0100

Ce mot-clé se nomme "directive". Il fait correspondre une ligne qui n'est là que pour informer le logiciel d'assemblage NASM et qui ne correspond pas à une instruction à assembler en tant que telle. Bien sûr, le 0x0100 pourrait également être écrit 100h, 256d ou encore 100000000b.

Maintenant que NASM sait où l'exécutable sera chargé, il va pouvoir nous donner l'adresse de n'importe quel endroit du code lors de son chargement. Pour repérer un endroit du programme, on utilise une étiquette. C'est un mot-clé placé en début de ligne et suivi des deux-points. Ce mot-clé ne doit pas être le même que celui d'une commande (mov, db, dw, org etc.), et ne peut être constitué que de caractères alphanumériques et du tiret-bas "_". Exemples :

etiquette:

mov ax, bx

transfert:

mov cl, bl

une_donnee:

db 47

Il est possible de mettre sur une seule ligne à la fois une étiquette et une instruction :

etiquette: mov ax, bx

transfert: mov cl, bl

une_donnee: db 47

Lorsqu'une étiquette est définie, on peut l'utiliser n'importe où dans le programme. Elle sera remplacée par la valeur qu'elle désigne lors de l'assemblage. On peut donc effectuer des opérations dessus, car c'est une valeur immédiate (valeur calculable dès de l'assemblage sans attendre l'exécution). Exemple :

etiquette: mov ax, bx

mov bx, etiquette

mov cx, transfert*2 + 5

transfert: mov cl, bl

mov dx, une_donnee

une_donnee: db 47

En revanche, il n'est pas possible de faire mov etiquette, registre ; cela n'aurait aucun sens car etiquette est une constante.
Il existe plein d'autres types d'étiquettes que nous verrons plus tard, car sinon, nous ne sommes pas près de faire notre "Hello World".

Vous n'avez pas compris les étiquettes ? Voici un exemple illustratif. Considérons le programme suivant :

org 100h

debut: db 0xE5

suite: dw 0x1234

fin: db 0x3D

Programme à évidemment ne pas tester puisque les données feraient office d'instructions. Nous sommes donc dans la situation suivante quand le code machine est chargé en mémoire vive :

Assignation d'adresses à des étiquettes Assignation d'adresses à des étiquettes

La colonne des étiquettes n'apparaît pas dans le code machine mais ne sert qu'au moment de l'assemblage : par exemple, toute référence à "suite" sera remplacée par "0x101".

Hello World

Le programme

Nous disposons de toutes les connaissances requises pour faire ce que l'on appelle chez les geeks programmeurs un "Hello World", c'est-à-dire un programme affichant simplement "Hello World". Comme nous n'avons pas vu les interruptions, ce programme servira d'introduction à cet outil, que je vais vous expliquer juste après avoir donné le code :

org 0x0100

; ceci est un programme affichant

; simplement "Hello World"

mov dx, texte

mov ah, 0x09

int 0x21 ; afficher le hello world

ret ; fin du programme

texte: db 'Hello, World !!', 10, 13, '$'

Le ret sert à sortir du programme, rien de plus à comprendre pour l'instant. Elle est obligatoire car si nous ne la mettions pas, l'exécution se poursuivrait au niveau du texte, et on ne sait pas à quelles instructions machine il correspond.

Le int correspond à l'appel d'un petit programme stocké dans l'ordinateur ; cela s'appelle une interruption. De nombreuses interruptions existent pour effectuer toutes sortes d'actions simples que nous fournit DOS ; celle-ci en est un exemple. On appelle le bout de code et celui-ci effectue une action en fonction de ce qu'on a mis dans AH. Ici, on y a mis 0x09 ce qui correspond pour l'interruption 0x21 à un affichage de texte. Cette routine prend en entrée l'adresse du texte à travers DX, et l'affiche.

texte: db 'Hello, World !!', 10, 13, '$' : le 10 indique un caractère spécial, et, suivi de 13, correspond à un retour à la ligne. On affiche donc "Hello World" puis un retour à la ligne, sinon, ce qui suit sera collé au texte. Pour dire à la routine DOS où se termine le texte (sinon, il risque de parcourir toute la mémoire), on met un '$'. Lorsque la routine voit ce caractère, elle se dit "C'est bon, j'ai fini !", s'arrête et repasse la main à notre programme sur l'instruction suivante, le "ret" dans notre exemple.

Assemblage et exécution

Sous Windows 16 bits

Créez un nouveau fichier texte d'extension .asm et enregistrez-le dans un nouveau dossier, par exemple "Assembleur". Ensuite, faites MAJ+clic-droit à l'intérieur du dossier, et sélectionnez "Ouvrir une fenêtre de commandes ici". Un truc bizarre ressemblant à ceci apparait sous vos yeux ébahis :

C:\Users\Codoscope\Desktop\Assembleur>_

Elle est pas belle ma console ? Assemblons. Pour cela, il faut entrer dans l'invite de commandes :

nasm -o [nom de l'exécutable].com [nom du fichier].asm

Par exemple, si votre fichier se nomme "crapule" mais que vous voulez donner à l'exécutable comme nom "fripouille" :

C:\Users\Codoscope\Desktop\Assembleur>nasm -o fripouille.com crapule.asm_

Si le nom de votre fichier comporte un ou plusieurs espaces, il faut le mettre entre guillemets, par exemple "crapule et fripouille.asm".
Ensuite, on frappe la touche entrée, le fichier est assemblé et la console vous donne une autre invite, comme si de rien n'était :

C:\Users\Codoscope\Desktop\Assembleur>nasm -o fripouille.com crapule.asm

C:\Users\Codoscope\Desktop\Assembleur>_

Et puis pour exécuter le programme, il suffit d'entrer son nom dans la console. Et sous vos yeux ébahis :

C:\Users\Codoscope\Desktop\Assembleur>nasm -o fripouille.com crapule.asm

C:\Users\Codoscope\Desktop\Assembleur>fripouille.com

Hello, World !!

C:\Users\Codoscope\Desktop\Assembleur>_

Splendide ! :ange:
On passe aux autres OS...

Les autres OS

Créez un nouveau fichier texte d'extension .asm et enregistrez-le dans un nouveau dossier (par exemple "asm"), disons dans le bureau pour faire simple. Avec la souris, saisissez ce fichier puis déplacez-le vers l'exécutable NASM pour l'assembler. Un nouveau fichier apparait, sans extension ; modifiez donc son nom pour ajouter un .com à la fin. On a donc un joli .com qui ne voudra pas être exécuté : magnifique, non ? Ce n'est pas grave, nous allons quand même y parvenir : téléchargez DOSBox (sous Ubuntu, vous pouvez passer par la logithèque). Installez-le et déplacez le fichier com sur l'exécutable pour l'ouvrir avec. Si jamais le fichier ne se finit pas par ".com", renommez-le pour ajouter cette extension.

Si vous ne parvenez pas à faire le cliquer-déplacer, voici comment faire en console sous Linux : ouvrez le Terminal :

Terminal de Linux Terminal de Linux

Allez dans le bureau à l'aide de la commande "cd" (attention à respecter la casse, c'est-à-dire mettre les majuscules quand il y en a) :

À n'importe quel moment, vous pouvez afficher le contenu du répertoire dans lequel vous êtes avec "ls". Entrez dans le dossier "asm".

Assemblez alors le programme "prog1.asm" de la même manière qu'avec Windows :

On peut à nouveau afficher le contenu du dossier :

Puis exécutez le programme avec DOSBox :

<

Faites Entrée :

Décidément, l'assembleur, c'est génial ! :D

N'hésitez pas à relire ce chapitre si vous n'avez pas tout compris et à refaire par vous-mêmes un programme qui affiche du texte sans regarder la solution.

#


ASM Mode Reel

 Manipulation et test des nombres Vu dans ... https://openclassrooms.com/

Connectez-vous ou inscrivez-vous pour bénéficier de toutes les fonctionnalités de ce cours !

Nous verrons dans ce chapitre des notions fondamentales de l'assembleur. N'hésitez pas à relire jusqu'à ce que compréhension s'ensuive !

L'adressage

Ici, nous nous contenterons d'utiliser l'adressage à travers les registres 32 bits et les valeurs immédiates (je rappelle que valeur immédiate = valeur connue lors de l'assemblage). De bien plus amples possibilités existent, mais nous les verrons plus tard.

En lecture

C'est une notion que l'on a déjà plus ou moins utilisée au chapitre précédent via l'étiquette repérant notre texte des plus originaux :

texte: db 'Hello, World !!', 10, 13, '$'

Cette étiquette nous a permis de donner l'adresse du texte à l'interruption DOS pour l'afficher. En gros, cette dernière a fait tout le boulot.
Mais l'interruption BIOS que nous allons utiliser est un peu plus flemmard ! Eh oui, il va falloir nous-mêmes lui donner une à une la valeur de chaque caractère : 'H', 'e', 'l' etc. Grâce à l'adressage, il suffit d'une adresse pour savoir ce qui s'y loge.

Pour cela, il existe de nombreux registres supplémentaires. >_ < D'accord, à notre stade, et vu que vous êtes pressés de passer à la suite, nous allons faire avec les registres généraux.

Pour désigner la valeur de quelque chose, on utilise son nom, par exemple superEtiquette ou EAX. Pour désigner ce qu'il pointe (la zone mémoire qui porte son adresse), on le met entre crochets, par exemple [superEtiquette] ou [EAX]. Et pour charger, on utilise comme d'habitude mov.

Quelques exemples histoire de comprendre quelque chose :

org 100h

mov AL, [genial] ; AL = 47h

mov AH, [ordi] ; AH = 2Eh

mov EBX,genial

mov CH, [EBX] ; CH = 47h

ret

genial: db 47h

ordi: db 2Eh

Ces crochets ne désignent qu'une syntaxe choisie par les inventeurs de NASM. Ils auraient tout aussi bien pu inventer un nouveau mot-clé, car, une fois assemblée, la ligne donne du code machine avec une instruction différente. Un petit schéma :

Attention, l'adresse n'est pas une chaîne de caractères mais est bien un nombre. Mais comme NASM remplacera chaque étiquette par sa valeur, on se fiche bien de combien ça fait et on peut l'utiliser comme une adresse numérique.

Bon, jusqu'à présent, c'était facile ; nous n'avons fait cela qu'avec de simples octets. Accrochez-vous car nous allons passer aux mots (rappel : un mot = 2 octets). Commençons par un exemple :

org 100h

mov AX, [etiquette] ; AX = 0xE3F6

mov EDX,etiquette

mov BX, [EDX] ; BX = 0xE3F6

ret

etiquette: dw 0E3F6h

Ça a l'air facile et logique, mais en fait, c'est plus compliqué que ça. Rappelez-vous, lorsque l'on utilise dw, les deux octets sont échangés :

Eh bien figurez-vous que les inventeurs du x86 ont fait ainsi car les octets sont aussi inversés lors d'un chargement de la mémoire vers un registre 16 bits :

Le chargement d'un mot de donnée avec inversion des octets

En écriture

C'est aussi simple que la lecture et on modifie la mémoire RAM. Mais attention, c'est dangereux. Ne modifiez que des données et non du code, car, encore une fois, le risque est de faire planter votre ordinateur. Exemple de programme :

org 0x100

mov AX,25

mov [toto],AX

mov CX, 11111

mov [titi],CX

ret

toto: resb 1

titi: dw 12

Une commande que nous n'avons pas encore vue : "resb" (REServe Byte) ; et il y a aussi "resw" (REServe Word). En fait, on aurait pu utiliser "db 0" ; mais la nouvelle commande est différente en deux choses :

La valeur de l'octet correspondante est inconnue, on sait juste que le système d'exploitation nous permet de l'utiliser.

Elle ne fait que réserver sans valeur ; donc, placé dans le disque dur, le programme prend moins de place.

Donc en fait, on a dans la mémoire vive l'accès à une case mémoire dont on ne connait pas la valeur... qu'à cela ne tienne ! Elle ne nous fournit aucune information mais nous permet comme db et dw d'en stocker pour la relire plus tard. On a ainsi le schéma :

N'oubliez pas qu'à travers AX et CX, ce sont deux octets qui sont modifiés en même temps.

Donc maintenant, nous savons modifier une case mémoire à travers un registre. Mais il est également possible de charger directement une valeur immédiate dans la RAM. On ne peut procéder ainsi ; le logiciel d'assemblage donnera une erreur :

mov [adresse],valeur ; ERREUR

En effet, comment savoir combien d'octets il faut charger ? Eh bien il suffit de le préciser en ajoutant le mot-clé byte ou word juste avant les crochets, et là, ça fonctionne :

mov byte[adresse],valeur ; OK, modifie 1 octet

mov word[adresse],valeur ; OK, modifie 2 octets

Les opérations

Comme les registres contiennent des nombres, on peut faire des additions, multiplications et autres opérations dessus. Commençons sans tarder !

Alors ne nous réjouissons pas trop vite car si l'ordinateur ne marche qu'avec des calculs, ils ne sont pas pour autant plus nombreux que sur une calculatrice 4 opérations. Et ne vous plaignez pas, car avant, seules les additions et soustractions étaient possibles (je pense au z80 sur lequel j'ai fait un tutoriel) et il fallait créer des algorithmes pour parvenir aux multiplications et divisions.

Incrémentation et décrémentation

Ce sont deux opérations très basiques : l'instruction inc pour incrémenter (ajouter 1) et dec pour décrémenter (retrancher 1). Elle est compatible avec les registres mais également avec l'adressage.

Voici un code à suivre linéairement pour voir ce qui se produit quand on exécute les instructions. Il permet également de revoir l'inversion des octets en mémoire ainsi que les limites de valeur des registres. Lisez-le tranquillement en essayant de comprendre ce qui se passe à chaque ligne. Prenez le temps qu'il vous faudra !

mov al,254 ; al = 254 (version non signée) / -2 (version signée)

inc al ; al = 255 (version non signée) / -1 (version signée)

inc al ; al = 0 pour les versions signée et non signée

; Revoir le cercle des nombres chapitre 2 en cas de doute

dec al ; al = 255 (version non signée) / -1 (version signée)

mov ah,0 ; ah = 0x00 & al = 0xFF

; => ax = 0x00FF

inc ax ; ax = 256 (version non signée) = 0x0100

; => ah = 0x01 = 1

; & al = 0x00 = 0

; byte[donnee] = 0x13 & byte[donnee+1] = 0x04

; => word[donnee] = 0x0413 (octets inversés !)

inc byte[donnee] ; byte[donnee] = 0x14 & byte[donnee+1] = 0x04

; => word[donnee] = 0x0414 (octets inversés !)

mov byte[donnee],0 ; byte[donnee] = 0 & byte[donnee+1] = 0x04

; => word[donnee] = 0x0400 (octets inversés !)

dec word[donnee] ; word[donnee] = 0x03FF (octets inversés !)

; => byte[donnee] = 0xFF

; & byte[donnee+1] = 0x03

ret

donnee: db 0x13, 0x04

Additions et soustractions

On utilise les mots-clé add et sub et on renseigne bien sûr les opérandes. Comme avec mov, c'est l'opérande de gauche qui prend le résultat (ce sera toujours comme ça). Le premier argument doit être un registre et le second argument soit un registre de même taille, soit une constante de valeur compatible.

mov ax,5

mov bx,12

add ax,bx ; ax = 17 ; bx = 12

add bx,ax ; ax = 17 ; bx = 29

add ax,30 ; ax = 47 ; bx = 29

sub ax,bx ; ax = 18 ; bx = 29

sub bx,15 ; ax = 18 ; bx = 14

; AX = 18 => AH = 0 et AL = 18

; BX = 14 => BH = 0 et BL = 14

add ah,bl

; ah = 14 = 0x0E

; al = 18 = 0x12

; donc ax = 0x0E12 = 3602

Multiplications et divisions euclidiennes

À partir de maintenant, j'utiliserai des mots-clé abrégés pour décrire plus rapidement la syntaxe c'est-à-dire les types d'opérandes qu'on a le droit de placer en argument. Voici la légende :

r = registre. Par exemple, BH est un registre.

m = donnée en mémoire. Par exemple, byte[adresse] est une donnée en mémoire.

imm = valeur immédiate, c'est-à-dire une constante. Par exemple, 5 et 78 sont des valeurs immédiates.

On commence par le premier :

mul r/m8

8 indique que l'on demande un opérande à 8 bits. Avec la barre, il est demandé soit un registre 8 bits, soit une zone mémoire 8 bits.
AL est multiplié par op1 ; le résultat est retourné dans AX.
À partir d'ici, quand je dirai "op1", cela signifiera "le premier opérande" (ici, il n'y en a qu'un : r/m8). Ça me permettra d'être plus court dans mes explications.

Exemple :

mul BL

mul r/m16

AX est multiplié par op1 ; le résultat est retourné dans le registre virtuel 32 bits DX:AX. En fait, le registre DX:AX est à DX et AX ce que BX est à BH et BL :

Constitution du registre virtuel DX:AX en comparaison avec BX Constitution du registre virtuel DX:AX en comparaison avec BX

C'est une technique introduite par le processeur 8086 dans lequel les registres 32 bits n'existaient pas. Et depuis, l'instruction n'a pas changé pour des raisons de compatibilité. Notez bien que le registre DX:AX n'existe pas ; on ne peut pas l'utiliser directement dans des instructions.

Exemple :

mov AX,4

mov BX,5

mul BX

; résultat = 20d = 0x14

; donc DX:AX = 0x14

; c'est-à-dire DX = 0 et AX = 0x14

mul r, r/m/imm

op1 est multiplié par op2 ; le résultat est placé dans op1.
(Ça marche de cette manière que ce soit 8 ou 16 bits, mais les deux opérandes doivent avoir le même nombre de bits.)

mul r, r/m, imm

op1 reçoit le produit de op2 par op3.

div r/m8

Divise AX par op1. Le quotient est retourné dans AL et le reste dans AH (puisque c'est une division euclidienne).

div r/m16

Divise DX:AX par op1. Le quotient est placé dans AX et le reste dans DX.

Voici un résumé :

mul r/m8 ; AX = AL*op1

mul r/m16 ; DX:AX = AX*op1

mul r, r/m/imm ; op1 = op1*op2 (avec op1 et op2 le même nombre de bits : 8 ou 16)

mul r, r/m, imm ; op1 = op2*op3 (avec op1, op2 et op3 le même nombre de bits : 8 ou 16)

div r/m8 ; AX/op1 ==> AL = quotient ; AH = reste

div r/m16 ; DX:AX/op1 ==> AX = quotient ; DX = reste

Les drapeaux

Introduction aux drapeaux

Jusqu'ici, nos programmes s'exécutaient de manière linéaire : les instructions se suivaient les unes après les autres, dans l'ordre, jusqu'à rencontrer l'instruction terminale "ret". Mais avec uniquement ça, on ne va pas très loin. Il faut bien que l'on puisse faire un programme qui adopte une réaction différente en fonction des informations qu'il reçoit. Par exemple, un jeu agira différemment en fonction des touches que presse l'utilisateur.

On aimerait qu'il y ait quelque chose pour nous avertir de si telle valeur est nulle, si telle autre est négative, tout comme un porte-drapeau nous avertirait de si l'on peut passer :

Un porte-drapeau Un porte-drapeau

Nos désirs sont exaucés : l'assembleur x86 contient des drapeaux (flags en anglais) qui nous donnent toutes sortes d'informations. Tout comme un vrai drapeau ou panneau n'a que deux côtés, un drapeau en assembleur n'a que deux valeurs possibles : 0 ou 1. Nous verrons plus tard qu'un drapeau est en fait un bit et que les drapeaux sont rangés dans le registre EFLAGS.

Les valeurs possibles d'un drapeau en assembleur Les valeurs possibles d'un drapeau en assembleur

Nous allons pour l'instant voir ceux qui se nomment les drapeaux de statut. Ceci n'est qu'une présentation et vous comprendrez ultérieurement ce qu'ils signifient vraiment.

CF : Carry Flag
Permet essentiellement de stocker d'éventuelles retenues, qui peuvent se montrer sur des instructions donnant lieu à des débordements de capacité. En décimal, quand on fait une opération à la main, une retenue est comprise entre 0 et 9. En binaire, elle sera comprise entre 0 et 1 : un bit (ce qui est logique puisqu'une retenue est un chiffre donc ici un chiffre binaire).

AF : Auxiliary carry Flag
Indique si la dernière opération a généré une retenue du bit 3 vers le bit 4.

OF : Overflow Flag
Encore un drapeau sur le débordement de capacité suite à une opération.

ZF : Zero Flag
Ce drapeau est modifié par certaines instructions pour indiquer si le résultat est égal à 0.

SF : Sign Flag
À la suite de certaines instructions, est modifié en fonction du signe du résultat d'une opération signée.

PF : Parity Flag
Détermine si le nombre de bits positionnés à un du résultat d'une opération est pair ou impair.

L'affectation des drapeaux

En assembleur, ce sont des instructions qui modifient la valeur des drapeaux. C'est par exemple le cas de l'instruction de comparaison :

cmp r, r/m/imm

Bien entendu, les deux arguments doivent faire le même nombre de bits. Soustrait VIRTUELLEMENT op2 à op1 (je dis bien virtuellement car le résultat n'est stocké nulle part). Les drapeaux sont affectés en conséquence :

ZF : à 1 si le résultat est nul (donc si op1 = op2)
SF : à 1 si le résultat de l'opération signée est négatif (donc si op1 < op2 en signé)
CF : à 1 si le résultat non signé est négatif (donc si op1 < op2 en non signé)

Par exemple :

mov BX, 15

cmp BX, 16

; ZF = 0

; SF = 1

; CF = 1

L'utilisation des drapeaux

En assembleur, chaque drapeau possède des mnémoniques qui permettent de constituer des instructions comme nous le verrons juste après. Ne vous laissez pas perturber par l'aspect repoussant de ce tableau ; la manière de l'utiliser est expliquée en dessous.

Mnémonique | Correspondance en anglais | Mot à mot | En français | Condition associée
Z E | Zero Equal | Zéro Égal | Nul Égal | ZF = 1
NZ NE | Not Zero Not Equal | Non zéro Non égal | Non nul Non égal | ZF = 0
C B NAE | Carry Below Neither Above nor Equal | Retenue En dessous Ni au-dessus, ni égal | Retenue Strictement inférieur Non supérieur | CF = 1
NC NB AE | Not Carry Not Below Above or Equal | Pas de retenue Pas en dessous Au-dessus ou égal | Sans retenue Non strictement inférieur Supérieur | CF = 0
O | Overflow | Dépassement | Dépassement | OF = 1
BE NA | Below or Equal Not Above | En dessous ou égal Pas au-dessus | Inférieur Non strictement supérieur | CF = 1 ou ZF = 1
NBE A | Neither Below nor Equal Above | Ni en dessous ni égal Au-dessus | Non inférieur Strictement supérieur | CF = 0 ou ZF = 0
NO | No Overflow | Pas de dépassement | Sans dépassement | OF = 0
S | Signed | Signé | Signé | SF = 1
NS | Not Signed | Non signé | Non signé | SF = 0
L NGE | Lower Neither Greater nor Equal | Inférieur Ni supérieur, ni égal | Strictement inférieur Non supérieur | soit SF = 1, soit OF = 1 (pas les 2 en même temps)
NL GE | Not Lower Greater or Equal | Pas inférieur Supérieur ou égal | Non strictement inférieur Supérieur | SF = 0 ou OF = 0
LE NG | Lower or Equal Not Greater | Inférieur ou égal Pas supérieur | Inférieur Non strictement supérieur | NGE = 1 ou ZF = 1
NLE G | Neither Lower nor Equal Greater | Ni inférieur ni égal Supérieur | Non inférieur Strictement supérieur | NGE = 0 ou ZF = 0
P PE | Parity Parity Even | Parité Parité paire | Parité Pair | PF = 1
NP PO | No Parity Parity Odd | Pas de parité Parité impaire | Sans parité Impair | PF = 0

Comme vous pouvez le voir, chaque condition possède plusieurs mnémoniques ayant exactement la même signification. NC, NB et AE veulent dire exactement la même chose à savoir CF = 0. C'est complètement inutile, je suis d'accord. Mais je préfère tout de même toutes vous les donner, sinon, vous pourriez être étonnés en lisant un code assembleur sur Internet, ne pouvant le comprendre.

Mais à quoi servent ces mnémoniques ? Tout d'abord, considérons le code suivant :

org 100h

mov ah,0x09 ; on se prépare à afficher du texte sous DOS

mov bx,10

mov cx,15

cmp bx,cx

; instruction manquante

PlusGrandOuEgal:

mov dx,superieur

int 0x21

ret

PlusPetit:

mov dx,inferieur

int 0x21

ret

superieur: db "BX est plus grand que CX", 10, 13, "$"

inferieur: db "BX est plus petit que CX", 10, 13, "$"

Notre but est de sauter à la bonne étiquette. Les étiquettes, nous les avons déjà vues pour repérer du texte. On peut bien sûr également repérer du code car il est quelque part dans est ainsi possible de demander de poursuivre l'exécution du programme autre part en fonction d'une condition.

Il faut faire le saut vers "PlusPetit" si BX est plus petit que CX. Quel mnémonique faut-il donc utiliser ?

C, B ou NAE ; c'est exactement la même chose.
Ces mnémoniques signifient que le résultat de la soustraction virtuelle est négatif.
Ils donnent une condition vraie lorsque BX - CX < 0, c'est-à-dire BX < CX.
Cela teste si l'opérande 2 (CX) est plus grand que l'opérande 1 (BX).

Pour faire un saut conditionnel, on utilise le mot-clé commençant par la lettre "J" suivie du mnémonique. Et on renseigne en argument l'endroit où sauter (l'étiquette d'arrivée). Cela donne une instruction demandant de poursuivre l'exécution ailleurs si la condition est vraie.

Que cela donne-t-il comme instruction ?

JC PlusPetit
JB PlusPetit
JNAE PlusPetit
(Les trois sont identiques : elles donneront le même code machine.)

N'oubliez pas qu'on peut mettre indifféremment en majuscules ou minuscules les mots-clés. Autrement dit, NASM ne respecte pas la casse pour les instructions. En revanche, pour les étiquettes, la casse a une importance ("PlusPetit" n'est pas pareil que "pluspetit").

Finalement, le code donne :

org 100h

mov ah,0x09 ; on se prépare à afficher du texte sous DOS

mov bx,10

mov cx,15

cmp bx,cx

jc PlusPetit

PlusGrandOuEgal:

mov dx,superieur

int 0x21

ret

PlusPetit:

mov dx,inferieur

int 0x21

ret

superieur: db "BX est plus grand que CX", 10, 13, "$"

inferieur: db "BX est plus petit que CX", 10, 13, "$"

Que se passe-t-il lors de l'exécution de ce programme ? Voyons ensemble :

Code exécuté | Ce qui se passe
Exécution des lignes 3, 4 et 5 Exécution des lignes 3, 4 et 5 | Diverses initialisations sont effectuées. AH = 0x09 BX = 10d CX = 15d Les valeurs des autres registres sont inconnues.
Exécution de la ligne 6 Exécution de la ligne 6 | Les drapeaux sont actualisés : ZF = 0 CF = 1 SF = 1
Saut de la ligne 7 à la ligne 12 Saut de la ligne 7 à la ligne 12 | Analyse du carry (CF) : il est actif. Le saut est donc effectué. L'exécution se poursuit à partir de l'endroit repéré par "PlusPetit". N'oubliez pas que "PlusPetit" disparaît lors de l'assemblage.
Exécution des lignes 13, 14 et 15 et utilisation de la ligne 18 Exécution des lignes 13, 14 et 15 et utilisation de la ligne 18 | L'adresse du texte repéré par "inferieur" est stockée dans DX. On appelle la routine DOS d'affichage. Puis le programme se termine avec "ret".

Dans les légendes, quand je parle des numéros de ligne, comprenez bien que ces lignes n'apparaissent pas dans le code machine. Quand je dis la ligne numéro n, il ne s'agit de rien d'autre que du code issu de l'assemblage de cette ligne.

Résultat :

C:\Users\Codoscope\Desktop\Assembleur>nasm -o fripouille.com crapule.asm

C:\Users\Codoscope\Desktop\Assembleur>fripouille.com

BX est plus petit que CX

C:\Users\Codoscope\Desktop\Assembleur>_

En fin de compte, tout s'est passé comme si notre programme avait été :

org 100h

mov ah,0x09

mov bx,10

mov cx,15

mov dx,inferieur

int 0x21

ret

inferieur: db "BX est plus petit que CX", 10, 13, "$"

Mais nos conditions deviendront plus utiles lorsque nous pourrons récupérer une entrée de l'utilisateur. Elles nous serviront même avant : à la fin de ce chapitre, pour faire un Hello World avec une routine BIOS.

En attendant, on peut optimiser notre programme. Que signifie optimiser ? C'est rendre un code plus réfléchi afin qu'il donne un exécutable :

plus léger ;

plus rapide.

Cela peut se faire en remarquant que seule la valeur de DX change en allant sur une étiquette ou sur l'autre. Voici le code simplifié :

org 100h

mov bx,10

mov cx,15

mov dx,superieur ; On suppose que BX >= CX (">=" signifie "supérieur ou égal à")

cmp bx,cx

jnc Suite

; si on est ici, c'est que BX < CX

; on remplace alors la mauvaise valeur par la bonne

mov dx,inferieur

Suite:

mov ah,0x09

int 0x21

ret

superieur: db "BX est plus grand que CX", 10, 13, "$"

inferieur: db "BX est plus petit que CX", 10, 13, "$"

N'hésitez pas à relire le code jusqu'à comprendre. Maintenant, supposons que l'on veuille comparer au début BX et DX, à la place de BX et CX. Un problème se montre alors : comment utiliser notre technique sachant que l'on "écraserait" la valeur de DX avant de l'avoir utilisée pour comparer ? On pourrait charger la valeur de DX vers BX pour revenir comme avant, mais il y a plus simple.

Pourquoi ne pas faire d'abord la comparaison, avant de faire mov dx,superieur ?
Je vous vois sceptiques, mais voyez plutôt :

org 100h

mov bx,10

mov dx,15

cmp bx,dx ; On active les drapeaux

mov dx,superieur ; Cette instruction ne change pas les drapeaux

jnc Suite ; On utilise les drapeaux précédemment activés

mov dx,inferieur

Suite:

mov ah,0x09

int 0x21

ret

superieur: db "BX est plus grand que DX", 10, 13, "$"

inferieur: db "BX est plus petit que DX", 10, 13, "$"

On utilise le résultat de la comparaison entre BX et l'ancienne valeur de DX (c'est-à-dire 15).

Faites vos tests vous-mêmes ! Par exemple, essayez de faire un programme qui compare "3*CX" et "BX/12 - 12". Vous pourrez également mettre 3 cas : supérieur, égal, inférieur. C'est en pratiquant qu'on apprend !

Avant de boucler cette section : il est possible de faire un saut qui ne dépend pas d'une condition. Le saut sera effectué de toutes façons. On utilise le mot-clé "jmp" (comme "jump" = saut) suivi d'un nom d'étiquette :

jmp etiquette ; le saut est toujours effectué mov

al,53 ; \ Ce code

mov bx,456 ; > ne sera

mov bh,al ; / pas lu

etiquette:

; suite du programme

Les instructions logiques

Nous allons voir ici les instructions logiques les plus importantes. Elles permettent d'agir sur les registres en tant qu'amas de bits plutôt que nombres. Si vous vous souvenez des composants que nous avons vus au premier chapitre, vous comprendrez facilement cette sous-partie.

4 instructions logiques

AND

and r/m, r/m/imm

Les bits sont analysés deux à deux : un pour op1, un pour op2. À chaque fois, un bit est renvoyé : 1 si les deux étaient à 1, sinon 0. Le résultat final est stocké dans op1.
Pour ces instructions logiques, il est habituel de donner une table de vérité ; je n’échapperai pas à la règle. Une table de vérité consiste à faire apparaître tous les cas possibles et donner à chaque fois le résultat. On nomme A et B les entrées et S la sortie.

A | B | S
0 | 0 | 0
1 | 0 | 0
0 | 1 | 0
1 | 1 | 1

Exemple : 11001110b and 10110010b ==> 10000010b

mov ah,11001110b

mov al,10110010b

and al,ah

; al = 10000010b

Cette instruction active le drapeau ZF.

OR

or r/m, r/m/imm

Le principe est le même que AND sauf que OR renvoie un 1 si au moins un des deux bits en entrée est à 1 :

A | B | S
0 | 0 | 0
1 | 0 | 1
0 | 1 | 1
1 | 1 | 1

Exemple :

mov ah,11001110b

mov al,10110010b

or al,ah

; al = 11111110b

XOR

xor r/m, r/m/imm

Cette fois, la sortie est à 1 si une et une seule des deux entrées est à 1 (autrement dit si les deux entrées ont une valeur différente) :

A | B | S
0 | 0 | 0
1 | 0 | 1
0 | 1 | 1
1 | 1 | 0

Exemple :

mov ah,11001110b

mov al,10110010b

xor al,ah

; al = 01111100b

NOT

not r/m

Inverse tous les bits de op1. Le résultat est naturellement renvoyé dans op1. Table de vérité :

A | S
0 | 1
1 | 0

Exemple :

mov ah,11001110b

not ah

; ah = 00110001b

Leur utilisation en pratique

Elles sont indispensables en assembleur même si pour l'instant cela peut vous paraître flou (vous comprendrez mieux par la suite). Mais voici tout de même pour l'instant quelques points spécifiques dont on peut se servir.

Le masquage de bits

Les instruction logiques que nous venons de voir permettent plusieurs choses intéressantes. Tout d'abord, avec OR, on peut faire ce qu'on appelle du masquage de bits. Par exemple, si l'on ne veut conserver dans AX que les 5 bits de poids fort :

and ax,11111000b

De poids fort = les bits les plus à gauche, donc ceux qui ont l'indice le plus grand.

Les 5 bits de poids faibles seront tous mis à 0. En effet, quelque soit A, si B=0, alors S=0.
De même, avec OR, on peut masquer des bits à 1 :

or ax,11111000b

Ici, les 5 bits de poids fort seront forcément à 1, et les autres auront une valeur inchangée.
Ça peut vous paraître inutile de faire joujou comme ça, mais vous comprendrez petit à petit à quoi ça sert.

L'activation des drapeaux

Les instructions logiques que nous avons vues agissent sur le drapeau ZF. ZF est à 1 si le résultat est nul (et inversement ZF est à 0 si le résultat est non nul). On peut alors effectuer une optimisation, lorsque l'on veut comparer un registre à 0 :

cmp dl,0

Il suffit alors de lui appliquer un OR ou AND avec lui-même :

or dl,dl

; ou

and dl,dl

Ces opérations, en y réfléchissant bien, ne changent pas la valeur de l'opérande car :

0 OR 0 = 0 ; 1 OR 1 = 1

0 AND 0 = 0 ; 1 AND 1 = 1

Par contre, elle agissent sur ZF.

L'initialisation nulle

La méthode la plus connue d'optimisation... vous vous en servirez tout le temps !
C'est une technique qui permet de mettre à 0 un registre de façon plus optimisée que avec "mov". On utilise "xor" et on renseigne 2 fois le même registre (attention, ça affecte les drapeaux) :

MOV CH,0 ; méthode lourde

XOR CH,CH ; méthode optimisée

Pourquoi ça marche ? Réfléchissons : pour chaque bit, le résultat sera soit "0 xor 0" soit "1 xor 1" puisque CH = CH. Les deux opérations valent 0. Donc cela ne va donner que des bits nuls. En fin de compte, ça renvoie 0.

Hello World : le retour

Affichage d'un caractère

Puisque nous sommes à la fin du chapitre, nous allons mettre en pratique ce que nous avons appris.

Nous allons donc réaliser un programme affichant du texte à l'aide d'une interruption BIOS. Ce qu'elle pose comme difficulté, c'est qu'elle n'affiche pas directement tout notre texte : elle ne fait qu'un caractère à la fois (ah non, on ne brusque pas Monsieur le BIOS). Donc on va devoir tout faire : lui donner les caractères un à un. C'est là que les sauts (conditionnels) vont servir, parce qu'il faut bien s'arrêter quand on est à la fin de la chaîne (histoire de ne pas afficher toute la RAM).

Pour appeler l'interruption, il faut mettre 0x0E dans AH (numéro de la fonction), puis appeler int 0x10 :

mov AH,0x0E

int 0x10

La routine demande en entrée :

AL = caractère à afficher

BH = page d'affichage (mettre 0)

CX = nombre de fois à afficher le caractère (mettre 1)

Exemple : construisez un programme qui affiche à l'écran le caractère "F".

org 0x100

mov AL,'F'

xor BH,BH ; eh oui, il faut optimiser !

mov CX,1

mov AH,0Eh

int 10h

ret

Pour vous épargner un clic pour savoir comment tester votre programme, je vous redis comment faire : MAJ + clic droit dans le dossier => "Ouvrir une fenêtre de commande ici". Et ensuite :

C:\Users\Codoscope\Desktop\Assembleur\nasm-2.09.10>nasm -o crapule.com crapule.asm

F

C:\Users\Codoscope\Desktop\ASSEMB~1\NASM-2~1.10>_

Bon, Windows abrège à sa façon le chemin si ça lui chante, mais notre programme marche.

Affichage de plusieurs caractères

C'est maintenant que ça se corse. But du jeu : extraire puis afficher chaque caractère un à un de notre texte. Pour commencer par le commencement, mettons ce qu'on peut mettre :

org 100h

Initialisation:

XOR BH,BH

MOV CX,1

MOV AH,0x0E

MOV EDX,texte

Traitement:

; Affichage des caractères

; du texte

Sortie:

ret

texte: db "Hello, World !"

Dans "Traitement", il va falloir :

Extraire le caractère

L'afficher

Sortir si on est à la fin du texte

Mais comment savoir si c'est fini ? La routine DOS avait choisi de s'arrêter lorsqu'elle rencontrait "$". C'est nul, car ça nous empêche d'afficher un "$". Nous, on va s'arrêter lorsqu'on rencontrera 0, qui ne correspond à aucun caractère. Essayez par vous-mêmes de compléter le code ! Il suffit d'aller vers "Sortie" si on a 0, et sinon de l'afficher puis d'aller vers "Traitement". Bien sûr, il faut aussi incrémenter EDX, qui pointe sur le caractère à afficher.

Voici une solution :

org 100h

Initialisation:

XOR BH,BH

MOV CX,1

MOV AH,0x0E

MOV EDX,texte

Traitement:

MOV AL,[EDX] ; Extraction du caractère

OR AL,AL ;\ Si le caractère

JZ Sortie ;/ est nul, c'est fini

INT 0x10 ; Sinon, on affiche

INC EDX ; On incrémente le pointeur

JMP Traitement ; Et on recommence

Sortie:

ret

texte: db "Hello, World !",0

Lancez le programme et voyez le résultat.

Vous venez d'apprendre... beaucoup de choses. :p
Elles serviront beaucoup par la suite, donc relisez-les si vous ne vous en souvenez plus.

Je vous conseille fortement d'effacer le programme et d'essayer de le refaire sans regarder.

#


Memoire

Électronique : LA MEMOIRE Vu dans ... https://openclassrooms.com/

Manipuler un ordinateur, c'est bien sympathique, surtout en assembleur, mais c'est encore mieux quand on sait ce qui se passe au niveau de l'électronique.

Ce chapitre n'est pas essentiel à la compréhension de la suite mais vous permettra cependant de comprendre comment la mémoire de l'ordinateur fonctionne. Cela signifie que si vous ne comprenez rien au chapitre, vous pouvez directement passer à la suite, et, si vous le souhaitez, y revenir ultérieurement.

Attention, il présentera la réalité de manière très simplifiée, son but étant de vous donner une vision globale du fonctionnement de la machine.

Les circuits logiques combinatoires

Nous avons déjà vu ce type de circuit : à chaque instant, la sortie dépend uniquement de l'entrée. On peut prendre pour exemples les portes que nous avons vues : AND, OR, NOT, et XOR. Par exemple, si je mets dans AND les entrées 0 et 1, la sortie sera forcément 0. On peut prévoir la ou les sortie(s) si on connaît la ou les entrée(s).

Prenons un autre exemple : notre circuit soigneusement préparé pour Noël :

Circuit logique combinatoire

Si je mets en entrées les bits 1001, on peut en déduire que les sorties seront 1 vers le ON de la machine à café, et 0 pour les deux autres.

Donc comprenez bien qu'avec un circuit logique combinatoire, les sorties ne dépendent que des entrées, et que donc rien ne l'empêchera de servir 50 fois le café de suite si on le lui demande, car il "oublie" à chaque fois ce que l'on a fait juste avant.

Les circuits logiques séquentiels

Ici, la logique des circuits combinatoires va être renversée car les sorties pourront ne pas être les mêmes à un instant ou à un autre, même si les entrées ne changent pas. À quoi cela peut-il bien servir ? Tout simplement pour faire de la mémoire : les circuits logiques séquentiels sont capables de "retenir" une information, ne serait-ce qu'un bit. On peut alors selon les cas lui demander de renvoyer ce bit, le changer ou ne rien faire (le conserver).

Commençons par faire simple : un circuit qui retient un bit mais que l'on ne peut pas changer :

Unité de mémoire Unité de mémoire

Eh oui, il va falloir commencer à se tordre la tête. :p
Pourtant, en réfléchissant un peu, le circuit n'a rien de bien méchant, et on peut constater que deux états possibles se montrent (rouge = le courant passe) :

Les deux états possibles de l'unité de mémoire Les deux états possibles de l'unité de mémoire

Ouf ! Il n'y a aucun paradoxe ! Eh bien je peux vous dire avec joie que nous avons conçu une cellule de mémoire, c'est-à-dire un bit, puisqu'il y a deux états possibles. Pour récupérer cet état, il suffit de brancher deux broches Q et ¯Q au circuit, en supposant que la broche Q indique l'état de celui-ci et ¯Q l'inverse de son état :

La lecture de l'unité de mémoire La lecture de l'unité de mémoire

Pour obtenir un octet, il suffit d'aligner 8 cellules comme celle-là. Seulement, notre mémoire est très limitée : on peut la lire, mais impossible de modifier son état !
On utilise alors une autre bascule que NOT : NOR. Voici un circuit séquentiel qui permet de sauver un bit, extraire sa valeur et la modifier :

Unité de mémoire reprogrammable Unité de mémoire reprogrammable

Vérifions que les deux états que le circuit peut avoir sont stables lorsqu'il n'y a aucune entrée :

Les deux états de l'unité de mémoire reprogrammable Les deux états de l'unité de mémoire reprogrammable

Tout est ok. De plus, on peut facilement récupérer l'état du circuit en utilisant les broches Q et ¯Q . Par exemple, on pourrait brancher une lampe qui montre son état. Intéressons-nous maintenant aux entrées R et S (Reset et Set). Elles sont censées mettre respectivement à 0 ou 1 l'état du circuit, quel que soit son précédent état. On utilisera alors la couleur verte pour un fil dans lequel on ne sait si le courant se propage ou non, et la couleur orange pour l'état inverse.

Reprogrammation d'unité de mémoire Reprogrammation d'unité de mémoire

Si vous ne comprenez rien au schéma, ce n'est pas grave, sachez juste que l'on peut modifier l'état du circuit à l'aide des entrées Set et Reset. On a donc bien enfin une cellule de mémoire, que l'on peut modifier et lire. Remarquez que si le circuit est mis soudainement hors-tension, le courant ne circule plus et toutes les données sont perdues. C'est pour cela que si vous êtes depuis 5 heures à travailler sur un document sans avoir enregistré et qu'il y a une panne de courant, tout est perdu sans aucun moyen de récupération !

Les registres

Après avoir manipulé ces mystérieux AX, BX et compagnie, vous allez maintenant pouvoir savoir ce qu'il se cache derrière tout cela. Ce que vous avez probablement déjà probablement compris, c'est que les registres sont de toutes petites zones mémoire dans le processeur, faisant à peine quelques octets. Tous les calculs et toutes les données passent par cette zone de registres.

On souhaite pouvoir, pour chaque bit de registre, pouvoir conserver sa valeur, écrire un bit spécifique ou lire sa valeur. Pour cela, on procède de la manière suivante :

Bit basculable en mode écriture Bit basculable en mode écriture

La "ligne de synchronisation" permet de dire si l'on passe en mode d'écriture. Quand elle est activée, elle permet de mettre "S" à 1 si la donnée est 1 et R à 1 si la donnée est à 0. Sinon, le courant ne passe ni dans R, ni dans S et la valeur est conservée. Pour la lecture, il suffit d'utiliser Q et ¯Q .

Maintenant, si l'on veut former tout un octet à lire et écrire, on aligne les bits. Pour simplifier le schéma, voici l'exemple d'un quartet :

Quartet basculable en mode écriture Quartet basculable en mode écriture

Il est ainsi possible d'écrire plusieurs bits en une seule fois. Pour écrire, il suffit de mettre à 1 la ligne de synchronisation et alors le quartet de mémoire absorbera ce qui est fourni en entrée.

La mémoire vive

Le principe d'enregistrement de données dans la mémoire vive est exactement le même, sauf qu'il faut pouvoir sélectionner chaque case mémoire indépendamment. Nulle question de mettre une ligne de synchronisation pour les millions de cases mémoire. Il s'agit en fait de sélectionner les emplacements mémoire avec leurs adresses : une zone mémoire sera active si l'adresse fournie en entrée correspond à la sienne.

Prenons le schéma précédent, étendons-le sur 8 bits et mettons-le dans une boîte pour simplifier le circuit qui va venir :

Octet basculable en mode écriture Octet basculable en mode écriture

La barre oblique avec un 8 signifie que le trait sur lequel il repose représente 8 fils donc 8 bits, dans un souci de simplification. Le but est donc d'activer le bon fil de synchronisation selon l'adresse. Par exemple, si l'adresse de cet octet est 10011011b, son fil de synchronisation doit être activé si et seulement si c'est l'adresse 10011011b qui est fournie au début du circuit. Créons une mémoire de 8 octets, donc adressable avec des adresses de 3 bits. Voici une manière de procéder :

Mémoire vive avec accès par adresse

Mémoire vive avec accès par adresse

On remarque que la broche de synchronisation d'un octet n'est à 1 que lorsque c'est son adresse qui est envoyée dans l'entrée en haut à gauche. Prenons l'exemple de la case mémoire d'adresse 001b. Elle est activée lorsque les trois broches en entrée du composant "AND" sont à 1, c'est-à-dire lorsque l'on a bit 1 = 0, bit 2 = 0 et bit 3 = 1, ce qui est bien l'adresse 001b.

Maintenant, il y a deux problèmes à résoudre : le premier est que lorsqu'on ne fait rien, l'adresse renseignée est zéro et donc la première case est sélectionnée de base en écriture... Il faut donc ajouter un quatrième bit en plus de l'adresse à mettre en entrée supplémentaire dans

tous les composants "AND". Par souci de schéma fastidieux à faire, je vous laisse vous représenter le résultat.

Le deuxième problème est le fait que la lecture de tous les octets se cumule. Il faut donc ajouter une activation d'un mode lecture qui permettra de faire que seul l'octet concerné par l'adresse envoie de la sortie. Cette activation se fait sur chaque bit :

Bit basculable en mode écriture ou lecture

Bit basculable en mode écriture ou lecture

On obtient alors un octet avec une entrée supplémentaire pour une activation de la lecture et le schéma de la mémoire vive se voit ajouter un cinquième bit.

Voilà, vous avez donc maintenant un aperçu simplifié du fonctionnement de la mémoire et de son adressage.


Notes sur le langage C

http://www.microelec.patricklecoq.fr

Accueil Electronique Informatique Réseaux Lexique
Composants | Dépannage | Dossiers | Réalisations | Les Connecteurs
Connecteurs internes | Entrées / Sorties | OS | Programmation | Vidéo | FAQ |
Modèle OSI | Protocole TCP | Protocole IP
Accueil / Micro / Réseaux / Adressage IP /

Retour en haut

Article basé du site Wikipédia , publiée sous licence GNU FDL .
Réseaux

Modèles OSI et TCP/IP Protocole TCP Protocole IP Adressage IP Les sous-réseaux Calcul réseaux

Réseaux hertziens Ressources
Présentation | Plan du site | Mentions Légales | Me contacter | © 1998-2011 Patrick LECOQ. Tous droits réservés. Révision : 24-02-2011


Notes sur le langage C

Le processeur d'un ordinateur ne comprend qu'un seul langage: le langage machine.
L'alphabet du langage machine d'un ordinateur est limité à deux états: 0 et 1 ou éteint et allumé ou Faux et Vrai, cet état est représenté sur un seul bit, c'est du binaire.

Présentation de l'Assembleur

Le langage assembleur (ASM) est, en programmation informatique, un langage de bas niveau qui représente le langage machine sous une forme lisible pour nous, humain.
On regroupe plusieurs bits pour constituer des octets (8 bits), des mots (16 bits) ou des double mots (32 bits).
Les combinaisons de bits du langage machine sont représentées par des symboles appelés "mnémoniques". Le programme assembleur convertit ces mnémoniques en langage machine en vue de créer par exemple un fichier exécutable.

Attention car l'assembleur représente exactement le langage machine et il est donc spécifique à chaque architecture de processeur. De plus, plusieurs groupes de mnémoniques ou de syntaxes de langage assembleur peuvent exister pour un seul ensemble d'instructions, créant ainsi des macro-instructions.

Les avantages de l'assembleur est la vitesse, car c'est le langage avec lequel on peut faire les programmes les plus rapides (si ils sont bien optimisés), une taille mémoire très réduite, l'accès aux interruptions DOS qui permettent un accès direct au matériel tel la souris, l'écran, ou la carte vidéo.
Le principale inconvénient est un gros investissement en heures de travail et en efforts par rapport aux langages de haut niveau comme le C++, le Java, etc. La solution est d'intégrée de l'assembleur dans un langage évolué.

Réversibilité du langage machine

Contrairement à un langage de haut niveau, il y a une correspondance un à un entre le code assembleur et le langage machine. Ainsi il est théoriquement possible de traduire le code dans les deux sens sans perte d'information. Cette opération s'appelle la Rétro-ingénierie (ou Reverse engineering). La transformation du code assembleur en langage machine est accomplie par un programme nommé assembleur, dans l'autre sens par un programme nommé désassembleur.

Assembleur 80x86

La famille 80x86 d’Intel commence à la fin des années70 (1978) avec le processeur 8086. Le x86 est la dénomination de la famille de microprocesseurs compatibles avec le jeu d'instructions du processeur 8086 de chez Intel. Afin que les anciens logiciels fonctionnent sur les nouveaux microprocesseurs qui ont suivi, les constructeurs de microprocesseurs pour PC doivent maintenir une compatibilité ascendante.
L'architecture de la série x86 à partir du Pentium a été nommée IA-32 par Intel.

Introduction

La représentation décimale

Les nombres en base 10 sont constitués de 10 chiffres de 0 à 9. On peut voir que le chiffre le plus à droite est multiplié par 1, le second par 10, le troisième par 100. En fait, chaque chiffre d'un nombre est associé à une puissance de 10 selon sa position dans le nombre. Exemple :
721 = 7 × 102 + 2 × 101 + 1 × 100

La représentation binaire

Les nombres en base 2 sont constitués de deux chiffres possibles 0 et 1. Ce sont les deux seules valeurs possibles du bit (b) qu'utilisent nos processeurs.
En partant de la droite, chaque chiffre d'un nombre est associé à une puissance de 2 croissante selon sa position dans le nombre. Exemple sur un octet:

 | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1
01110010b = | 0 x 27 + | 1 x 26 + | 1 x 25 + | 1 x 24 + | 0 x 23 + | 0 x 22 + | 1 x 21 + | 0 x 20

= 64 + 32 + 16 + 2
cela fait 114 en base 10.

Les nombres négatifs

Le signe d'un nombre est représenté par 1 bit. C'est le bit de gauche d'un ensemble de bits qui sera le bit de signe. Si le bit de signe est à 1, le nombre est négatif. S'il est à 0 alors le nombre est positif.

Décimal | Binaire
-128 | 10000000
-1 | 11111111
0 | 00000000
1 | 00000001
+127 | 01111111

Sur un octet (8 bits), lorsqu'il est signé (+ ou -), il peut prendre une valeur comprise entre -128 et +127. Lorsqu'il n'est pas signé, il peut prendre une valeur comprise entre 0 et +255.

Complément à un

Cette opération code de façon à inverser la valeur de chaque bit composant une valeur binaire.
Aussi, pour obtenir la valeur -5 :

5 (décimale) => 0101 (binaire) donne 1010 complément à un.

Complément à deux

On fait un complément à un de la valeur binaire et on ajoute 1 au résultat.
Exemple:

  1. 25 (décimale) donne 00011001 (binaire)
  2. par complément à un, on a: 11100110
  3. en ajoutant 1, on obtient: 11100111

Le complément a deux de 00011001 est donc 11100111. La représentation binaire signée de -25 est donc
11100111.

L'octet

Pour une meilleure lisibilité on regroupe plusieurs bits pour constituer des octets (8 bits). L'octet (en anglais byte ou B) est donc une unité d'information composée de 8 bits.
Un regroupement de 16 bits est appelée mot (word).
Un regroupement de 32 bits de longueur est appelée double mot (double word, dword).
Un regroupement de 64 bits de longueur est appelée quadruple mot (quadword, qword).

La représentation hexadécimale

Les nombres en base 2 deviennent vite très grand d'où la notation hexadécimale (base 16). Comme 16 est une puissance de 2, la conversion binaire - hexadécimal est simple: 4 chiffres binaire correspond à 1 chiffre hexadécimal.
Les chiffres en base 16 (hexadécimal) sont représenté par un nombre hexadécimal suivi du caractère H (exemple: 2FC4H). Un chiffre hexadécimal peut prendre les valeurs 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, et F avec :

Base 16 | A | B | C | D | E | F
Base 10 | 10 | 11 | 12 | 13 | 14 | 15

Dans le tableau ci-dessous, le chiffre hexadécimal 2FC4H donne la valeur 0010111111000100b en binaire.
Hexadécimal | Décimal | Binaire
2 | 2 | 0010
F | 15 | 1111
C | 12 | 1100
4 | 4 | 0100


Notes sur le langage C

La mémoire

Les instructions occupent un ou plusieurs emplacements consécutifs de la mémoire.
Chaque caractère occupe un emplacement mémoire et les chaines de caractères occupent plusieurs emplacements consécutifs.
Les valeurs numériques occupent autant d'emplacements consécutifs que nécessaire.

Les possibilités d'adressage mémoire de la gamme x86 remontent au 8080, qui avait 16 bits de bus d'adresse et pouvait donc adresser 64 Kio de mémoire.

Lors de l'arrivée du 8086, Intel introduit l'adressage segmenté. La mémoire d'un ordinateur est une série d'emplacement. Le numéro d'un emplacement est appelé adresse (un emplacement est un ensemble de 8 bits ou byte). La mémoire est divisée en zone de 65 536 bytes appelées segments. L'adresse d'un byte est composée d'un numéro de segment et d'un déplacement nommée offset calculé par rapport au début du segment.

Segment : Offset (B000 :0007 représente le huitième octet du segment n° B000).

L'adresse effective peut être calculer avec la formule: adresse effective = 16 × Segment + Offset

Segmentation de la mémoire

Le mode réel

Avec le 8086 le bus d'adresse est sur 20 bits offrant un adressage limité à seulement un méga octet. Les adresses allant de 00000H à FFFFFH. Cependant, un nombre de 20 bits ne tiendrait dans aucun des registres 16 bits du 8086.
Intel a résolu le problème en segmentant la mémoire en bloc de 64 Kio appelés segment. La valeur de segment peut être stockée dans un registre (16 bits) dédié:

  • Le segment de code CS (Code Segment), il contient l'adresse du segment de la mémoire où se trouvent les instructions du programmes (code machine).
  • Le segment de données DS (Data Segment), il contient l'adresse du segment de mémoire contenant les données définies dans le programme.
  • Le segment de pile SS (Stack segment), il contient l'adresse du segment qui contient la pile..

Ce mode de fonctionnement est utilisé à l'initialisation de l'ordinateur et en mode DOS.
Dans ce mode, un programme peut accéder à n'importe quelle adresse mémoire, même la mémoire des autres programmes!

Le mode protégé

En 1982, le 80286 introduit le mode protégé. En mode protégé, les segments ne se réfèrent plus à l'adresse segment mais à des nouveaux registres comme GDTR, LDTR et IDTR qui maintiennent en plus des informations de protection.
Le mode protégé utilise une technique appelée mémoire virtuelle. L'idée de base d'un système de mémoire virtuelle est de ne garder en mémoire que les programmes et les données actuellement utilisés. Ce travail est effectué de façon transparente par le système d'exploitation. 		Niveaux Privileges

L'espace adressable est de 16 Mio, l'espace virtuel est potentiellement de 1 Gio, un segment ne peut pas dépasser 64 Kio

Le 80386 a introduit le mode protégé 32 bits. Le 80386 ayant des registres sur 32 bits les déplacements se font sur 32 bits. Les segments peuvent être aussi gros que tout l'espace adressable, soit 4 Gio.
Chaque programme reçoit une table de descripteur de segments indépendante (table de descripteurs locaux = Local descriptor table, LDTR). Chaque segment peut être indépendant de tous les autres segments utilisés par les autres applications.

La pagination vient s'ajouter à la segmentation.
Dans ce mode, le segment peut être divisé en plusieurs blocs mémoire de 4 Kio appelés page. Ce mode permet d'écrire sur le disque une partie du code qui n'est pas en cours d'exécution. Cela évite de charger l'ensemble de l'application en mémoire. Par contre, cela ralentit beaucoup l'exécution.

Les registres x86 64 bits

Avec les processeurs 64 bits de nouveaux registres apparaissent. Les registres 32 bits sont étendus à 64 bits. Ainsi le registre EAX est étendu à 64 bits et devient RAX.
En mode 64 bits, on peut toujours accéder à :

  • EAX : partie basse 32 bits (bits 0 à 31) de RAX.
  • AX : partie basse 16 bits (bits 0 à 15) de EAX.
  • AH : partie haute 8 bits (bits 8 à 15) de AX.
  • AL : partie basse 8 bits (bit 0 à 7) de AX.

En plus des registres 64 bits

  • RAX dont les 32 bits de poids faible sont constitués du registre EAX
  • RBX dont les 32 bits de poids faible sont constitués du registre EBX
  • RCX dont les 32 bits de poids faible sont constitués du registre ECX
  • RDX dont les 32 bits de poids faible sont constitués du registre EDX
  • RDI dont les 32 bits de poids faible sont constitués du registre EDI
  • RSI dont les 32 bits de poids faible sont constitués du registre ESI
  • RBP dont les 32 bits de poids faible sont constitués du registre EBP
  • RSP dont les 32 bits de poids faible sont constitués du registre ESP
il y a 8 nouveaux registres 64 bits, de R8 à R15 et sont utilisables tout comme les autres registres généraux.

Registres X86

Le mode 32 bits en exemple sur la représentation à gauche où les quatre registres de travail sont principalement utilisés pour stocker des résultats :

  1. EAX : registre accumulateur ( accumulator register ). Utilisé pour les opérations arithmétiques et le stockage de la valeur de retour des appels systèmes.
  2. EBX : registre de base ( base register ). Utilisé comme pointeur de donnée (située dans DS en mode segmenté).
  3. ECX : registre compteur ( counter register )
  4. EDX : registre de données ( data register ). Utilisé pour les opérations arithmétiques et les opérations d'entrée/sortie.

Ce sont des registres 32 bits; pour des raisons historiques, les 16 bits de poids faible sont constitués respectivement des registres AX, DX, CX et BX.

Ces 4 registres 16 bits sont également décomposés en 8 registres de 8 bits :

  1. AL : octet de poids faible de AX
  2. AH : octet de poids fort de AX
  3. BL : octet de poids faible de BX
  4. BH : octet de poids fort de BX
  5. CL : octet de poids faible de CX
  6. CH : octet de poids fort de CX
  7. DL : octet de poids faible de DX
  8. DH : octet de poids fort de DX

Les registres d'offset

Les registres d'offset (32 bits) sont utilisés lors de l'adressage indirect de la mémoire (pointeurs). Ces registres complémentaires sont :

  1. ESP : ( Extended Stack Pointeur ) pointeur de pile
  2. EBP : ( Extended Base Pointer ) pointeur de base
  3. ESI : ( Extended Source Index ) pointeur source
  4. EDI : ( Extended Destination Index ) pointeur destination

Le nom des deux derniers registres vient du fait qu'ils sont utilisés pour la copie d'une zone mémoire vers une autre.
Ces 4 registres 32 bits sont également décomposés en 4 registres de 16 bits respectivement SP, BP, SI et DI.

Les registres de segment

La gestion de la mémoire dans l'architecture x86 est particulière : celle-ci est divisée en segments. Les registres de segment (16 bits) permettent d'accéder, soit au segment de programme qui est la zone mémoire des instructions de programme, soit au segment de données (zone mémoire contenant les données du programme), ou encore au segment de pile.

  1. CS: pointe vers les instructions du programme ( code segment ).
  2. DS : pointe vers les données du programme ( data segment ).
  3. SS : pointe vers la pile programme ( stack segment ).
  4. ES : pointe vers les données du programme multi-segments ( extra segment ).
  5. FS : pointe vers les données du programme multi-segments en mode protégé.
  6. GS : pointe vers les données du programme multi-segments en mode protégé.

La gestion de la mémoire en multi-segments permet d'utiliser tous les registres d'offset. La plupart des systèmes d'exploitation actuels utilisent un mode protégé où tous les registres de segment pointent vers le même segment.

Le registre d'états

Le registre RFLAGS (64 bits) avec respectivement EFLAGS (32 bits) et FLAGS (16bits), chaque bit du registre RFLAGS est un indicateur d'état qui peut être modifié à chaque instruction exécutée.

RFLAGS
Bits | 63..32 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | 15 | 14 | 13..12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
Drapeaux | - | - | - | - | - | - | - | - | - | - | - | ID | VIP | VIF | AC | VM | RF | 0 | NT | IOPL | OF | DF | IF | TF | SF | ZF | 0 | AF | 0 | PF | 1 | CF

Les drapeaux d'état

Les bits 0, 2, 4, 6, 7 et 11 du registre RFLAGS indiquent les résultats d'opérations arithmétiques résultant d'instructions telles que ADD, DIV, MUL, SUB, etc.

  • CF (bit 0) Carry Flag (Drapeau de retenue) : Ce drapeau est mis à 1 si une opération arithmétique génère une retenue sur le bit le plus significatif (bit de poids fort). Le drapeau est mis à 0 dans les autres cas. Ce drapeau indique ainsi une condition de débordement en arithmétique entière non signée. Il est aussi utilisé pour l'arithmétique en précision multiple.
  • PF (bit 2) Parity Flag (Drapeau de parité) : Mis à 1 si l'octet de poids faible (octet le moins significatif) du résultat généré après une opération arithmétique contient un nombre pair de bits à 1. Le drapeau est mis à 0 dans les autres cas.
  • AF (bit 4) Adjust Flag (Drapeau d'ajustement) : Mis à 1 si le résultat d'une opération arithmétique génère un résultat provoquant une retenue sur le troisième bit. Le drapeau est mis à 0 dans les autres cas. Ce drapeau n'est utile que dans l'utilisation du codage BCD.
  • ZF (bit 6) Zero Flag (Drapeau zéro) : Mis à 1 si le résultat d'une opération arithmétique vaut zéro. Le drapeau est mis à 0 dans les autres cas.
  • SF (bit 7) Sign Flag (Drapeau de signe) : Mis à 1 si le résultat d'une opération arithmétique possède un bit de poids fort (bit le plus significatif) à 1, indiquant ainsi un nombre signé. Le drapeau est mis à 0 dans les autres cas (indiquant possiblement un résultat non signé, c'est-à-dire positif).
  • OF (bit 11) Overflow Flag (Drapeau de débordement) : Mis à 1 si le résultat constitue un nombre positif ou négatif (en excluant le bit de signe) ne pouvant tenir dans l'opérande de destination. Le drapeau est mis à 0 dans les autres cas. Ce drapeau indique une condition de débordement pour les opérations arithmétiques signées sur les entiers.
Le drapeau de contrôle

Le bit 10 du registre RFLAGS est le seul drapeau de contrôle.

  • DF (bit 10) Direction Flag (Drapeau de direction) : Ce drapeau est utilisé conjointement avec les instructions opérant sur les chaînes de caractères. Lorsque le drapeau DF est à 1, les adresses des chaînes de caractères sont auto décrémentées (allant ainsi des adresses les plus hautes vers les adresses les plus basses). Lorsque le drapeau est à 0, les adresses des chaînes de caractères sont auto incrémentées (allant des adresses les plus basses vers les adresses les plus hautes. Aucun résultat d'opération ne permet d'armer ou de désarmer le drapeau de direction. Seules deux instructions permettent de spécifier explicitement son état.
Les drapeaux système

Les drapeaux système du registre RFLAGS sont contrôlés par le système d'exploitation ou les opérations de surveillance système. En temps normal un programme applicatif ne devrait pas modifier l'état de ces drapeaux. Les bits 12 et 13 ne sont pas considérés comme deux drapeaux, mais comme un champ de 2 bits (le champ IOPL).

  • TF (bit 8) Trap Flag (Drapeau de trappe) : Lorsqu'il est à 1, ce drapeau permet le débogage en mode pas à pas, c'est-à-dire instruction par instruction. Lorsqu'il est à 0, le mode pas à pas est inopérant (fonctionnement normal).
  • IF (bit 9) Interrupt Flag (Drapeau d'interruption) : Ce drapeau contrôle la façon dont le processeur répond aux requêtes d'interruptions masquables (c'est-à-dire désactivables). Lorsqu'il est à 1, le processeur peut répondre à toutes les interruptions, dans le cas contraire (drapeau IF à 0), le processeur ne pourra répondre qu'aux interruptions non masquables.
  • IOPL (bits 12 et 13) Input / Output privilege level field (Champ de niveau de privilège d'entrée et de sortie) : Ce champ indique le niveau de privilège en entrée/sortie (E/S) du programme ou de la tâche courante. Le niveau de privilège courant du programme ou de la tâche en cour doit être égal ou inférieur au niveau de privilège d'E/S pour accéder à l'espace d'adressage. Ce champ ne peut être modifié qu'avec un niveau de privilège égal à 0 (niveau de privilège le plus haut). Ce concept de niveaux de privilèges est implémenté au travers des anneaux de protection.
  • NT (bit 14) Nested task Flag (Drapeau de tâche chaînée) : Ce drapeau contrôle l'enchaînement des tâches interrompues et appelées. Il indique ainsi, lorsqu'il est à 1, si la tâche courante est liée à une tâche parent (la tâche qui s'exécutait auparavant) via l'instruction CALL ou par le biais d'une interruption. Lorsqu'il est à 0, ce drapeau indique simplement que la tâche courante n'a pas de tâche parente.
  • RF (bit 16) Resume Flag (Drapeau de redémarrage) : Ce drapeau contrôle la réponse du processeur aux exceptions de débogage. Il assure notamment que le débogage en pas à pas (voir drapeau TF) n'intervient qu'une seule fois par instruction.
  • VM (bit 17) Virtual-8086 mode Flag (Drapeau de mode virtuel 8086) : Lorsque ce drapeau est à 0 le processeur est en mode virtuel 8086. Lorsqu'il est à 0, le processeur revient en mode protégé.
  • AC (bit 18) Alignment Check Flag (Drapeau de vérification d'alignement) : Ce drapeau, lorsqu'il est à 1, assure une vérification d'alignement des références mémoire. Lorsqu'il est à 0, aucune vérification d'alignement n'est effectuée. Ce drapeau nécessite d'armer conjointement le bit AM du registre de contrôle CR0.
  • VIF (bit 19) Virtual Interrupt Flag (Drapeau d'interruption virtuelle) : Ce drapeau est une image virtuelle du drapeau IF. Il est utilisé en conjonction avec le drapeau VIP (bit 20).
  • VIP (bit 20) Virtual Interrupt Pending Flag (Drapeau d'interruption virtuelle en attente) : Lorsqu'il est à 1ce drapeau indique qu'une interruption est en attente. Lorsqu'il est à 0 ce drapeau indique qu'aucune interruption n'est en attente. Seuls les programmes peuvent armer ou désarmer ce drapeau, le processeur ne fait que le lire. À utiliser conjointement avec le drapeau VIF (bit 19).
  • ID (bit 21) Identification Flag (Drapeau d'indentification) : Si un programme à la possibilité d'armer ou de désarmer ce drapeau, cela indique que le processeur supporte l'utilisation de l'instruction CPUID.

Le pointeur d'instruction

Le registre RIP (64 bits) comprenant respectivement EIP (32 bits) et IP (16bits) est utilisé avec le segment du registre CS par le processeur pour connaitre la prochaine instruction à exécuter. Ce registre est donc modifié implicitement par le processeur (instruction suivante, saut à l'adresse indiquée, appel d'une fonction, interruption ...).

Registres divers

  • Registres de contrôle CR0 à CR3 et CR8 à CR15(Control Register).
  • Registres de mise au point DR0 à DR7 et DR8 à DR15(Debug Register).
  • Registres de test TR3 à TR7 (Test Register).
  • Registres de la table de descripteur Global GDTR (Global Descriptor Table Register).
  • Registres de la table de descripteur d'interruptions IDTR (Interrupt Descriptor Table Register).
  • Registres de la table de descripteur local LDTR (Local Descriptor Table Register).
  • Registres de tâches TR (Task Register).
  • Registres de datation TSC (Time Stamp Counter).Ce registre accumule le nombre de cycles d'horloge depuis le démarrage de l'ordi ou depuis le dernier reset.
  • Registres 64 bits MMX de MM0 à MM7.
  • Registres 128 bits SIMD de XMM0 à XMM7 et XMM8 à XMM15.


Notes sur le langage C

ADRESSAGE IPv4

Une adresse IPv4 permet d'identifier chaque machine connectée sur un réseau informatique utilisant le protocole IP version 4.

Représentation

Une adresse IP est codée sur 4 octets. Cette adresse se présente sous la forme "xxx.xxx.xxx.xxx" où "xxx" est situé entre 0 et 255(notation en décimal). Les numéros s'étendent donc de 0.0.0.0 à 255.255.255.255.
En fait, ces quatre groupes "xxx" représente chacun un octet, donc une adresse IP est codée sur 4 fois 8 bits soit 32 bits : cela donne au maximum 232 soit 4 294 967 296 adresses IPv4 possible.
En représentation binaire, une adresse IP est comprise entre:
00000000.00000000.00000000.00000000. et 11111111.11111111.11111111.11111111.

Principe

Lorsqu'un ordinateur tente de communiquer sur le réseau IP, il se sert du masque de sous -réseau:

  • Il fait un ET logique entre son adresse IP et le masque de sous -réseau. Il obtient son ID réseau.
  • Il fait un ET logique entre l'adresse IP de destination et le masque.
    Si le résultat est identique à l'ID réseau local précédemment calculé alors il envoie le paquet vers le réseau local.
    Si le résultat est différent à l'ID réseau local alors l'ordinateur envoie le paquet vers la passerelle par défaut. La passerelle devient alors responsable du routage du paquet vers le réseau adéquat.

Le masque

L'adresse IP est constituée de deux parties: un identificateur de réseau et un identificateur de la machine (hôte).

ID réseau ID hôte
32 bits

Le masque de sous-réseau permet de savoir quelle partie d'une adresse IP correspond à la partie ID réseau et laquelle correspond à la partie ID hôte.

Un masque est constitué de 32 bits comme l'adresse IP. Pour calculer la partie ID réseau d'une adresse IP, on effectue une opération ET logique bit à bit entre l'adresse IP et le masque.
Exemple avec l'adresse 192.168.0.1 et le masque par défaut 255.255.255.0:

Convertissons l'adresse décimal 192.168.1.1 en binaire avec le tableau ci-dessous:
Nous avons 192 = 128 + 64
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
1 x 27 + | 1 x 26 + | 0 x 25 + | 0 x 24 + | 0 x 23 + | 0 x 22 + | 0 x 21 + | 0 x 20 | = 1100 0000b
Donc
192. | 168. | 0. | 1 | Décimal
1100 0000 | 1010 1000 | 0000 0000 | 0000 0001 | Binaire
255. | 255. | 255. | 0 | Décimal
1111 1111 | 1111 1111 | 1111 1111 | 0000 0000 | Binaire

Effectuons un ET logique avec le masque de sous-réseau

 | (192. | 168. | 0. | 1)
 | 1100 0000 | 1010 1000 | 0000 0000 | 0000 0001
et |  |  |  |
 | (255 | 255 | 255 | 0)
 | 1111 1111 | 1111 1111 | 1111 1111 | 0000 0000
= | 1100 0000 | 1010 1000 | 0000 0000 | 0000 0000
soit | 192. | 168. | 0. | 0

Pour l'adresse 192.168.0.1 et le masque par défaut 255.255.255.0 nous avons:

192. | 168. | 0. | 1
ID réseau |  |  | ID Hôte

Résumé: Le masque de sous-réseau consiste en l'attribution de 1 aux bits de l'identificateur réseau, et de 0 aux bits de l'identificateur d'hôte.
Ressource: Calcul d'un masque et d'un réseau.

Adressage IP dynamique

L'adresse IP d'un ordinateur sur un réseau local lui est transmise et assignée au démarrage grâce au protocole DHCP (Dynamic Host Configuration Protocol).

Adressage IP statique

L'adresse IP d'un ordinateur est fixée manuellement dans la configuration du système d'exploitation. Les serveurs sont en IP statique notamment le serveur DHCP.

Adresse IP unique

Chaque machine, raccordée à un réseau IP, est identifiée par une adresse IP. Cette adresse IP doit être unique sur le réseau.
Chaque paquet transmis via le protocole IP contient dans son en-tête l'adresse IP de l'émetteur ainsi que l'adresse IP du destinataire (adresses IP source et destination). Cela permet aux équipements du réseau de "router" les paquets jusqu'à destination grâce à l'adresse IP. Le destinataire saura ainsi à qui renvoyer les données grâce à l'adresse IP de l'émetteur contenu dans les en-têtes des paquets envoyés.
Il est donc nécessaire que chaque composant connecté à un réseau informatique dispose d'une adresse IP unique pour pouvoir communiquer avec les autres équipements.

Conventions

  • L'ID réseau ne doit pas être 127 (boucle locale). L'adresse 127.0.0.1 est une adresse de bouclage local (localhost, Loopback). Les autres adresses dans le réseau 127.0.0.0/8 sont considérées comme locales, de même que celles du réseau 0.0.0.0/8.
  • L'ID réseau et Hôte ne doivent pas être tous à 0. L'adresse 0.0.0.0 est illégitime en tant qu'adresse de destination, elle est interprétée comme ce "réseau local".
  • L'ID réseau et Hôte ne doivent pas être tous à 1. L'adresse 255.255.255.255 est une adresse de diffusion.
  • La première adresse d'un réseau spécifie le réseau lui-même, la dernière est une adresse de diffusion (broadcast) sur ledit réseau.

Les classes

C'est InterNic qui alloue les adresses Internet. Ces adresses IP se divisent en trois classes pour les plus courantes: Les classes A, B et C.

Classe | Bits de | Bits | Bits | Adressage théorique
 | poids fort | réseaux | hôtes
A | 0 | 7 | 24 | 27=128 réseaux et 224=16 777 216 hôtes; De 0.0.0.0 à 127.255.255.255/8
B | 10 | 14 | 16 | 214=16 384 réseaux et 216=65 535 hôtes; De 128.0.0.0 à 191.255.255.255/16
C | 110 | 21 | 8 | 221=2 097 152 réseaux et 28=256 hôtes; De 192.0.0.0 à 223.255.255.255/24
D | 1110 | 28 |  | Multicast De 224.0.0.0 à 239.255.255.255/4
E | 11110 | (Réservé aux expérimentations) De 240.0.0.0 à 255.255.255.255/4

Classe A

De 0.xxx.xxx.xxx à 127.xxx.xxx.xxx
Les 8 premiers bits identifient le réseau et les 24 autres identifient les hôtes

27 -2 = 126 réseaux possibles car 0.xxx.xxx.xxx et 127.xxx.xxx.xxx sont réservés. Un seul de ces réseaux peut accueillir 224 -2 machines, soit 16 777 214 ordinateurs.

  • Adresse IP: [1à126].xxx.xxx.xxx
  • Adresse de réseau: [1à126].0.0.0
  • Adresse de broadcast: [1à126].255.255.255
  • Masque: 255.0.0.0

Classe B

Les 16 premiers bits identifient le réseau et les 16 autres identifient les hôtes

214 = 16 384 réseaux possibles avec 216 -2 soit 65 534 ordinateurs par réseau.

  • Adresse IP: [128à191].xxx.xxx.xxx
  • Adresse de réseau: [128à191].xxx.0.0
  • Adresse de broadcast: [128à191].xxx.255.255
  • Masque: 255.255.0.0

Classe C

Les 24 premiers bits identifient le réseau et les 8 autres identifient les hôtes

221 = 2 097 152 réseaux possibles avec 28 -2 soit 254 ordinateurs par réseau.

  • Adresse IP: [192à223].xxx.xxx.xxx
  • Adresse de réseau: [192à223].xxx.xxx.000
  • Adresse de broadcast: [192à223].xxx.xxx.255
  • Masque: 255.2555.255.0

Plages d'adresses privées

Les adresses suivantes ne sont pas routées sur Internet [RFC 1918]: elles sont réservées à un usage local :

  • De 10.0.0.1à 10.255.255.254 (10/8 ou 10.0.0.0/255.0.0.0) soit plus de 16 millions d'adresses.
  • De 172.16.0.1à 172.31.255.254 (172.16/12 ou 172.16.0.0/255.240.0.0) soit plus de 1 million d'adresses.
  • De 192.168.0.1à 192.168.255.254 (192.168/16 ou 192.168.0.0/255.255.0.0) soit près de 65000 adresses.

APIPA (Automatic Private Internet Protocol Addressing) [Link Local - RFC 3330] ou IPv4LL est une plage réservée auprès l'IANA qui permet à un système d'exploitation de s'attribuer une IP automatiquement (en cas d'absence de configuration manuelle explicite et de non-détection d'autres systèmes de configuration comme le DHCP).

  • De 169.254.0.1 à 169.254.255.254 (169.254/16)

CIDR

En 1992 la moitié des classes B étaient prises et si le rythme avait continué, il y aurait eu pénurie d'adresse IP sur internet. De plus la croissance du nombre de réseaux se traduisait par un volume important de routes à l'intérieur des routeurs, lesquels arrivaient à saturation.

Aussi le CIDR (Classless InterDomain Routing ou routage interdomaine sans classe) est mis en place par l'L'IETF. Le CIDR réduit le nombre de route stockées sur un routeur et augmente le nombre d'adresses IP disponibles.

Le CIDR offre la possibilité de créer:

  • Des sous-réseaux en ajoutant des bits à 1 au masque de sous-réseau qui permet de le diviser en sous-réseau.
    Exemple: un masque de sous-réseau /27 au lieu de /24 désigne la subdivsion du réseau en 8 sous-réseaux.
  • Des super-réseaux en empruntant des bits de l'ID réseau et les masquant comme ID d'hôte.
    Exemple: un masque de sous-réseau /22 au lieu de /24 désigne un bloc de quatre classes C consécutives

La notation CIDR définit une convention d'écriture qui spécifie le nombre de bits utilisés pour identifier la partie réseau (ID réseau), c'est à dire les bits à 1 du masque. (RFC 1518 et 1519 définissent le CIDR (Classless Inter-Domain Routing))
Les adresses IP sont alors données sous la forme :

10.0.0.1/8 <=> 10.0.0.1 255.0.0.0
172.16.0.1/16 <=> 172.16.0.1 255.255.0.0
192.168.0.1/24 <=> 192.168.0.1 255.255.255.0
192.168.0.1/16 <=> 192.168.0.1 255.255.0.0
192.168.0.1/25 <=> 192.168.0.1 255.255.255.128

VLSM

VLSM (Variable-Lengh Subnet MASK ou masque de sous-réseau à longueur variable) se sont des masques de sous-réseau variant entre routeurs d'un même réseau. Cela permet une plus grande souplesse dans les plages d'adresses de sous-réseau.

CIDR et VLSM repose sur des protocoles sans classe donc incompatible avec RIP v1. L' OSPF, IS-IS, RIP-2, BGP-4 le sont.


Notes sur le langage C

Directive d'assemblage

En plus de coder les instructions machine, les langages assembleur ont des directives supplémentaires pour assembler des blocs de données et assigner des adresses aux instructions en définissant des étiquettes ou labels.
Ils sont capables de définir des expressions symboliques qui sont évaluées à chaque assemblage, rendant le code encore plus facile à lire et à comprendre.

L'assembleur reconnaît un sous-ensemble de caractères de la séquence ASCII, c'est à dire les caractères de l'alphabet (non accentués), les caractères numériques et les caractères comme + - * / = ( ) [ ] ; ' . ! , _ : @ $ , l'espace, TAB, RETURN, LF.
Les différents champs d'un programme assembleur doivent être séparés par au moins un espace. Les opérandes dans une instruction sont séparées par une virgule.
Un programme en assembleur comprend:

  • Un champ étiquette (identificateur, label), qui peut être vide;
  • Un champ mnémonique (le nom de l’instruction);
  • Un champ opérandes (les arguments de l’instruction);
  • Un champ commentaire.
Labels | Instructions | Opérandes | Commentaires
Debut | mov | ax,0 | ; Instruction MOV est repérée par le Label "Debut"
 | nop |  | ; Instruction seul (ici pas d'opération)
 | mov | bx,7 |
 | jmp | Debut | ; Force le processeur à retourner au label 'Debut", l'exécution du programme continuera alors séquentiellement.

Définition de constantes

La directive EQU associe une valeur à un nom qui pourra être ensuite utilisé dans le programme à la place de la constante qu’elle définit.

Vrai | EQU 1 | ; "Vrai" a la valeur 1
Faux | EQU 0 | ; La constante "Faux" a la valeur 0

Définition de variables

Les variables sont des zones mémoires qui sont réservées lors de l'assemblage.

DB : Variable 8 bits

Cette instruction définit des zones mémoires de 1 Byte. Elle est aussi utilisée pour définir des chaines de caractères.

Status | DB 0 | ; Définition d'un octet initialisé à 0
xyz | DB 1,2,3 | ; Définition d'une zone de 3éléments de type Byte
autre | DB 7 DUP (1,2,3,4,5,6,7) | ; Définition d'une table
 | DB "Toto", 0 | ; Définition d'une chaîne de caractères terminée par un NULL

DW : Variable 16 bits

Cette instruction définit des zones mémoires de 2 Bytes ou un mot (word). Le début de la zone mémoire est repéré par un identificateur.

Cent | DW 1000 | ; Définition d'une variable WORD (2 Bytes) un mot initialisé à 1000
Trois | DW 1, 2, 3 | ; Définition d'un tableau de trois mots initialisés à 1, 2, 3
TableW | DW 5 DUP(?) | ; Définition d'un tableau de 5 éléments de type Word (16 bits)

DD : Variable 32 bits

Cette instruction définit des zones mémoires de 4 Bytes ou un double mot (double word).

Adresse1 | DD 0F70ABCDh | ; Définition un mot double initialisé à 0xF70ABCD
Trois | DD 100,200,300 | ; Définition d'un tableau de trois double-mots initialisés à 100, 200, 300
Table DW | DD 10 DUP(0) | ; Définition d'un tableau de 10 valeurs initialisées à 0

A la dernière ligne un exemple avec la directive DUP qui permet de déclarer un tableau de n cases, toutes initialisées à la même valeur.

Compilation

L’assembleur est un utilitaire qui n’est pas interactif. Le programme que l’on désire traduire en langage machine (assembler) doit être placé dans un fichier texte avec l’extension .asm.

  1. Pour la saisie du programme source (.asm) on utilise un simple éditeur de texte capable de générer un fichier ASCII.
  2. L’opération d’assemblage traduit chaque instruction du programme source (.asm) en une instruction machine. Le résultat de l’assemblage est enregistré dans un fichier objet avec l’extension .obj. Cette étape est aussi appelée compilation du programme réalisé par le compilateur.
  3. Le fichier .obj n’est pas directement exécutable.
    En effet, il arrive fréquemment que l’on construise un programme exécutable à partir de plusieurs fichiers sources. Il faut “relier” les fichiers objets à l’aide d’un utilitaire nommé éditeur de lien (même si l’on a qu’un seul). L’éditeur de liens fabrique un fichier exécutable, avec l’extension .EXE ou .COM.

Les fichiers .EXE ou .COM sont directement exécutables. Un utilitaire spécial (chargeur) du système d’exploitation (Ex: DOS), est responsable de la lecture du fichier exécutable, de son implantation en mémoire principale, puis du lancement du programme.
Les programmes .EXE sont limités que par la mémoire disponible dans l’ordinateur contrairement aux programmes .COM qui ne peuvent pas utiliser plus d’un segment dans la mémoire. Leur taille est ainsi limitée à 64 Kio.
Les fichiers .COM débute à l’offset 100h et non à l’offset oh.

Voici une liste non- exhaustive d'assembleur:

  • FASM: Flatassembler.net;
  • MASM: Masm32.com;
  • NASM: Nasm.us et chez.com/asmgges/nasm.html;
  • RosASM: Betov.free.fr;
  • TASM: Faqs.org/faqs/assembly-language/x86/borland

Avec Linux

Ici, je vais vous présenter Nasm. NASM (Netwide Assembler) est un logiciel assembleur libre pour architecture x86, utilisant la syntaxe Intel. On peut le trouver pour plusieurs systèmes d'exploitation comme Linux, Windows, FreeBSD, Mac OS X, OS/2.
Il peut être utilisé pour produire à la fois des programmes 16 bits et 32 bits (IA-32) et depuis la version 2 de NASM il est possible de produire aussi des programmes 64 bits (X64/AMD64/EMT64/x86-64).

Création du fichier source

Vous n'avez pas besoin d'un éditeur spécifique pour créer un fichier source destiné à Nasm. Vous pouvez utiliser Gedit, Kwrite, Xemacs, etc.
Ce fichier aura l'extension .asm.

Assemblage du fichier source

Si ce n'est pas déjà fait, vous allez installer Nasm sur votre machine. Si vous êtes sous Ubuntu ou plus généralement sous Debian, tapez dans un terminal la commande: " sudo apt-get install nasm"

Exemple:
patrick@calypso:~$ sudo apt-get install nasm
Lecture des listes de paquets... Fait
Construction de l'arbre des dépendances
Lecture des informations d'état... Fait
Les NOUVEAUX paquets suivants seront installés :
nasm
0 mis à jour, 1 nouvellement installés, 0 à enlever et 0 non mis à jour.
Il est nécessaire de prendre 0o/840ko dans les archives.
Après cette opération, 2081ko d'espace disque supplémentaires seront utilisés.
Sélection du paquet nasm précédemment désélectionné.
(Lecture de la base de données... 128969 fichiers et répertoires déjà installés.)24-02-2011asm_0.99.06-2ubuntu0.1_amd64.deb) ...
Paramétrage de nasm (0.99.06-2ubuntu0.1) .

Si vous avez une autre distribution Linux, vous devez utiliser le gestionnaire de paquet de votre distribution (urpmi, yum, ).
Après l'installation de Nasm, créez votre fichier source: essai.asm par exemple.

mov ax,123
nop
mov dx,456
nop
mov ah,9
nop
int 0x21
nop
nop
nop

Dans le répertoire où se situe votre fichier source avec l'extension .asm:
Tapez la commande: "nasm -f essai.asm". Nous avons créé un fichier binaire. Ce fichier peut-être utilisé seulement dans des cas bien particuliers.

Code hexa
On peut voir ici les différentes instructions séparées par des NOP (Opcode: 90).

Dans un cas plus général:
Créez un fichier source: Tempo.asm.
; Temporisation avec 2 boucles imbriquées
_start:
mov cx,7FFFH
; Initialisation de CX à 7FFF
Temp1:
push cx
; Sauvegarde de CX dans la pile
mov cx,5FFFH
; Chargement de CX par 5FFF
Temp2:
nop
; Pas d'opération
nop
; juste pour passer le temps
nop
nop
nop
loop Temp2
; Décrémente CX et effectue une boucle à l'étiquette Temp2 tant que CX est non nul.
pop cx
; Restauration de CX de la pile
loop Temp1
; Décrémente CX et effectue une boucle à l'étiquette Temp1 tant que CX est non nul.
ret

Tapez la commande: "nasm -f elf Tempo.asm". Ceci créera un fichier objet nommé "Tempo.o" dans le répertoire courant. Ce fichier n'est pas encore exécutable, ce n'est encore qu'un fichier objet, c'est à dire un fichier qu'il faut lier avec les bibliothèques dont il dépend comme par exemple la libraire standard libc.

Création de l'exécutable

Avec notre fichier objet nommé Tempo.o nous allons créer un exécutable.
Deux cas possible:

  • Le programme débute avec une procédure nommée "_start". Le programme possède alors son propre point d'entrée sans utiliser la fonction main. Vous exécuter la commande "ld" pour créer l'exécutable.
    "ld Tempo.o -o Tempo"
  • Le programme commence avec une procédure nommée "main". Vous allez exécuter la commande "gcc" pour créer l'exécutable.
    "gcc Tempo.o -o Tempo "

L'exécutable est ainsi créé dans le répertoire courant avec "tempo" comme nom.

Exécution du programme

Pour exécuter notre programme, il suffit de taper cette commande: "./Tempo"
Vous pouvez également utiliser un débogueur pour observer l'exécution. Pour cela vous pouvez utiliser gdb qui s'utilise en ligne de commande. Si toutefois vous voulez un débogueur plus visuel, il existe des interfaces graphiques(GUI) qui utilisent gdb en arrière plan comme par exemple Insight ou Nemiver (débogueur C\C++).

Avec Windows

Ici, je vais vous présenter Fasm. FASM (Flat asembler) est un programme assembleur pour l'architecture IA-32 (x86). FASM est écrit en langage assembleur et existe pour les systèmes DOS, DexOS, GNU/Linux, Windows, et Menuet.
FASM a quelques caractéristiques évoluées pour un langage assembleur tel que les macros, les structures, et les données virtuelles. FASM intègre des bibliothèques pour l'environnement graphique Windows et OpenGL.
On peut utiliser FASM en ligne de commande avec l'exécutable "fasm.exe" ou avec l'IDE avec la commande "fasmw.exe".

Création du fichier source

Pour la saisie du programme source (.asm) on utilise un simple éditeur de texte. On peut citer: Editpad Lite, Notepad++, Ultraedit, etc. On peut utiliser aussi Fasm. Tapez "fasmw.exe" et rentrez le programme suivant:

Avec Flat asembler 1.67.27, fichier source "helloword.asm":

; Affichage facile de "Hello, World!"

org 100h
jmp start
msg: db "Hello, World!", 0Dh,0Ah, 24h

start: mov dx, msg
mov ah, 09h
int 21h

mov ah, 0
int 16h

ret

; Création d'un programme .com
; Saut à "start" par dessus les data declaration.

; Charge offset de msg dans dx.
; Service 9 de l'interruption21h.
; Exécution

; Attente d'une touche....

; Retour

Compilation du fichier source

Dans le menu "Run" cliquez sur "Compile". Un fichier "helloword.com" vient d'être créer.

Exécution du programme

Ouvrez l'interpréteur de commande de Windows (tapez "cmd" dans "Exécuter" du menu démarrer ou directement "cmd" dans la barre "recherche" du logo Windows sous Windows Vista.

Avec l'interpréteur de commande, vous devrez vous rendre dans le dossier qui contient le fichier "helloword.com" avec la commande DOS "cd".

Lancez le programme en tapant: "helloword". 		Hello word

Mise au point et désassemblage

Un désassembleur est un programme qui traduit le langage machine en langage assembleur. Le désassemblage est l'inverse de celle effectuée par un programme assembleur, l'assemblage.
Attention, le désassemblage ne restitue pas les noms de variables, les étiquettes, les noms de procédures ni même les commentaires.

On peut citer: Ndisasm (inclus dans Nasm) ou Debug (Commande DOS).

L'utilitaire Debug permet d'assembler, de désassembler et faire de la mise au point de programme, bref il permet de faire ses premiers pas en assembleur.

Liste des commandes Debug

Commandes | Syntaxes
Aide en ligne | ? (Affiche une liste des commandes de Debug)
Assemble (Assemble des mnémoniques 8086/8087/8088) | A (adresse)
Compare (Compare deux zones de la mémoire) | C plage adresse
Dump (Affiche le contenu d'une zone de la mémoire) | D (plage)
Enter (Entre des données en mémoire a partir d'une adresse donnée) | E adresse (liste)
Fill (Remplit une zone de mémoire avec des valeurs données) | F plage liste
Go (Execute le fichier exécutable charge en mémoire) | G (=adresse) (adresses)
Hex (Réalise des opérations arithmétiques sur des valeurs hexadécimales) | H valeur1 valeur2
Input (Affiche la valeur d'un octet d'un port donne) | I port
Load (Charge en mémoire un fichier ou des secteurs de disque) | L (adresse) (lecteur) (secteur) (nombre)
Move (Copie le contenu d'un bloc de mémoire) | M plage adresse
Name (Spécifie un nom de fichier pour les commandes L et W ou spécifie les paramètres pour le fichier mis au point) | N (nchemin) (listearg)
Output (Envoie la valeur d'un octet vers un port de sortie) | O port val
Proceed (Execute une boucle, une instruction sur chaine répétée, une interruption logicielle ou une sous routine) | P (=adresse) (nbre)
Quit (Quitte Debug) | Q
Register (Affiche ou modifie le contenu d'un ou plusieurs registres) | R (registre)
Search (Recherche, dans une zone de mémoire, une séquence d'octets donnée) | S plage liste
Trace (Execute une instruction puis affiche le contenu de tous les registres, l'état de tous les indicateurs et la forme décodée de l'instruction que Debug exécutera ensuite) | T (=adresse) (valeur)
Unassemble (Désassemble des octets et affiche les instructions source correspondantes) | U (plage)
Write (Ecrit le fichier en cours de mise au point sur disque) | W (adresse) (lecteur) (secteur) (nombre)
Allocate expanded memory (Alloue de la mémoire paginée EMS) | XA (#pages)
Deallocate expanded memory (Désactive l'allocation d'un descripteur de mémoire paginée EMS) | XD (desc)
Map expanded memory pages (Affecte des pages logiques aux pages physiques de la mémoire paginée) | XM (pageL) (pageP) (desc)
Display expanded memory status (Affiche des informations sur l'état de la mémoire paginée EMS) | XS

Instructions de base

Il existe plusieurs catégories d'instruction: les instructions de base et d'affectation, les instructions arithmétiques et logiques, les instructions de décalage et de rotation, les instructions de comparaison et les instructions de rupture de séquence.

  1. De base et d'affectation
     Les instructions d'affectation permettent de faire des transferts de données entre les registres et la mémoire.
    • Chargement d'une valeur dans un registre
    • Déplacement d'une valeur depuis un emplacement mémoire dans un registre, et inversement
    • Exemple: (avec mov est sans doute l'instruction la plus utilisée)
      mov eax, 0123 | ; on met la valeur 0123 dans le registre EAX
      mov bx,ax | ; le contenu de AX dans le registre BX
      mov Mem1, ax | ; Transfert du contenu du AX vers une case mémoire d ’adresse Mem1
      mov ax,Mem2 | ; Transfert du contenu d’une case mémoire 16 bits Mem2 vers AX
      Pour le 8086:
      24-02-2011td>Fonctions
      Usage | Instructions
      Général | MOV | Transfert d'un ou plusieurs octets
      PUSH | Chargement de la pile
      POP | Déchargement de la plie
      PUSHA | Chargement de tous les registres dans la pile
      POPA | Déchargement de tous les registres de la pile
      XCHG | Echange d'un ou plusieurs octets
      XLAT | Translation d'octet
      Entrées Sorties | IN | Entrée d'un ou plusieurs octets
      OUT | Sortie d'un ou plusieurs octets
      Adresses | LEA | Chargement d'adresse effective
      LDS | Chargement du pointeur ave DS
      LES | Chargement du pointeur avec ES
      Indicateurs | LAHF | Transfert des indicateurs dans AH
      SAHF | Rangement de AH dans les indicateurs dans la pile
      PUSHP | Chargement des indicateurs dans la pile
      POPF | Déchargement des indicateurs de la pile

  2. Arithmétiques et logiques
    • Addition, soustraction, multiplication et division des valeurs de deux registres ou mémoire et chargement du résultat dans un registre ou une mémoire.
      Exemple:
      add | bx,2 | ; Ajoute 2 dans BX, le résultat est placé dans BX
      add | bx,ax | ; Ajoute le contenu de AX dans BX
      add | ax,[bx+2] | ; Ajoute le contenu de la zone mémoire à l'adresse DS:BX+2 dans AX

      Usage | Instructions | Fonctions
      Addition | ADD | Addition
      ADC | Adition avec retenue
      INC | Incrémentation de +1
      AAA | Ajustement ASCII
      DAA | Ajustement décimal
      Soustraction | SUB | Soustraction
      SBB | Soustraction avec retenue
      DEC | Décrémentation de -1
      NEG | Mètre un ou plusieurs octets en négatif (complément à deux)
      AAS | Ajustement ASCII
      DAS | Ajustement décimal
      Multiplication | MUL | Multiplication d'un ou plusieurs octets non signé
      IMUL | Multiplication d'un ou plusieurs octets signé
      AAM | Ajustement ASCII
      Division | DIV | Division d'un ou plusieurs octets non signé
      IDIV | Division d'un ou plusieurs octets signé
      AAD | Ajustement ASCII

    • Combinaison de valeurs de deux registres suivant une opération booléenne (ou opération bit à bit)
      Exemple:
      L'instruction AND effectue un ET LOGIQUE sur les 2 opérandes, le résultat est placé dans la première opérande. Le AND est utilisé pour isoler certains bits d'un octet(s) ou pour mettre à 0 un ou plusieurs bits. On l'utilise aussi pour calculer le reste d'une division.
      Calcul de 35 / 8 (=4 et reste 3)
      mov | ax,35 | ; 35 dans AX
      mob | bx,8 | ; 8 dans BX
      and | ax,bx | ; = 0000 0011 dans AX (soit 3 en décimal: reste de la division dans AX)

      Instructions | Fonctions
      NOT | Inversion logique de tous les bits
      AND | ET logique sur les opérandes
      OR | OU logique entre les opérandes
      XOR | OU exclusif entre les opérandes
      TEST | ET logique, les indicateurs rlètent le résultat

  3. Décalage et de rotation
    Usage | Instructions | Fonctions
    Décalages | SHL | Décalage logique à gauche
    SAL | Décalage arithmétique à gauche
    SHT | Décalage logique à droite
    SAR | Décalage arithmétique à droite
    Rotation | ROL | Rotation à Gauche
    ROR | Rotation à droite
    RCL | Rotation à gauche avec bit de retenue
    RCR | Rotation à droite avec bit de retenue

    ROL RCL
    L'instruction ROL décale tous les bits vers la gauche de une ou plusieurs positions. Les bits qui "sortent" par la gauche sont réinjectés par la droite, l'indicateur de retenue CF est perdu.
    Exemple:
    mov ax, 0xC123H
    rol ax, 1 | ; ax = 8247H, CF = 1
    rol ax, 1 | ; ax = 048FH, CF = 1
    rol ax, 1 | ; ax = 091EH, CF = 0
    ror ax, 2 | ; ax = 8247H, CF = 1
            L'instruction RCL décale tous les bits vers la gauche de une ou plusieurs positions. Les bits qui "sortent" par la gauche passent par l'indicateur de retenue CF (CARRY FLAG) avant d'être réinjectés par la droite.
    Exemple:
    mov ax, 0xC123H
    clc | ; eteint le drapeau de retenue (CF = 0)
    rcl ax, 1 | ; ax = 8246H, CF = 1
    rcl ax, 1 | ; ax = 048DH, CF = 1
    rcl ax, 1 | ; ax = 091BH, CF = 0
    rcr ax, 2 | ; ax = 8246H, CF = 1
    rcr ax, 1 | ; ax = C123H, CF = 0

  4. Comparaison
    • Comparer les valeurs de deux registres

      Instructions | Fonctions
      CMP | ; Compare 2 opérandes en soustrayant la seconde à la première. Les opérandes ne sont pas modifiées et les indicateurs reflètent le résultat
      CMPSB | ; Compare les Bytes (Octets)
      CMPSW | ; Compare les Words (mots ou 2 Bytes)

  5. Modification du déroulement du programme
    • Saut à un autre emplacement dans le programme (normalement, les instructions sont exécutées séquentiellement, les unes après les autres)
    • Saut à un autre emplacement, mais après avoir sauvegardé l'instruction suivante afin de pouvoir y revenir (point de retour)
    • Retour au dernier point de retour

      Branchements | Instructions | Fonctions
      inconditionnels | CALL | Sous programme
      RET | Retour sous programme
      JMP | Saut
      conditionnels arithmétique non signée | JA - JNBE | Si supérieur - Si non inférieur et non égal
      JAE - JNB | Si supérieur ou égal - Si non inférieur
      JB - JNAE | Si inférieur - Si non supérieur et non égal
      JBE - JNA | Si inférieur ou égal - Si non supérieur
      conditionnels arithmétique signée | JG - JNLE | Si plus grand - Si pas inférieur et non égal
      JGE - JNL | Si plus grand ou égal - Si pas inférieur
      JL - JNGE | Si moins que - Si pas plus grand et pas égal
      JLE - JNG | Si moins que ou égal - Si pas plus grand
      conditionnels | JC | Si retenue
      JE - JZ | Si égal - Si zéro
      JNC | Si pas de retenue
      JNE - JNZ | Si pas égal - Non zéro
      JNO | Si pas de débordement
      JNP - JPO | Si pas de parité - Si parité impaire
      JNS | Si pas de signe
      JO | Si débordement
      JP - JPE | Si parité - Si parité paire
      JS | Si signe négatif
      Boucles | LOOP | Boucle
      LOOPE - LOOPZ | Boucle si égal - Si zéro
      LOOPNE - LOOPNZ | Boucle si différent - Si différent de 0
      JCXZ | Branchement si CX=0
      Interruptions | INT | Interruption
      INTO | Interruption si débordement
      IRET | Retour d'interruption


Notes sur le langage C

Instructions des 8086/8088

Nom | Sens | Traduction | Remarques
AAA | A SCII A djust AL after A S ubtraction | Ajuste le registre AL après soustraction (mode ASCII) | Utilisé avec le codage BCD non compacté
ADC | Ad d with C arry | Ajoute deux entiers, plus le drapeau de retenue | Le résultat remplace le premier opérande
ADD | Add | Ajoute deux entiers | Le résultat remplace le premier opérande
AND | Logical AND | Effectue un ET logique des opérandes | Le résultat remplace le premier opérande
CALL | Call Procedure | Appelle une procédure |
CBW | C onvert B yte to W ord | Convertit un octet en mot | Le registre AL est étendu à AX
CLC | Cl ear C arry Flag | Met le drapeau de retenue à zéro |
CLD | Cl ear D irection Flag | Met le drapeau de direction à zéro |
CLI | Cl ear I nterrupt Flag | Met le drapeau d'interruption à zéro |
CMC | C o m plement C arry Flag | Inverse le drapeau de retenue |
CMP | C o mp are | Compare deux entiers (de façon arithmétique) | Positionne les drapeaux en fonction de la différence entre les opérandes
CMPSzz | C o mp are S trings | Compare un octet/mot de deux chaines | Mnémoniques : CMPS, CMPSB, CMPSW
CWD | C onvert W ord to D oubleword | Convertit un mot en double mot | Le registre AX est étendu à DX:AX
DAA | D ecimal A djust AL after A ddition | Ajuste le registre AL après soustraction (mode décimal) | Utilisé avec le codage BCD compacté
DAS | D ecimal A djust AL after S ubtraction | Ajuste le registre AL après soustraction (mode décimal) | Utilisé avec le codage BCD compacté
DEC | Dec rement by 1 | Décrémente un entier | Ôte un de l'opérande
DIV | Unsigned Div ide | Divise par un entier non signé | Le dividende est le registre AX/DX:AX, le quotient est écrit dans AL/AX et le reste dans AH/DX
ESC | Esc ape |  | Utilisé avec l' unité de calcul en virgule flottante
HLT | H a lt | Entre en état d'arrêt |
IDIV | S i gned Div ide | Divise par un entier signé | Le dividende est le registre AX/DX:AX, le quotient est écrit dans AL/AX et le reste dans AH/DX
IMUL | S i gned Mul tiply | Multiplie par un entier signé | Le facteur est le registre AL/AX et le produit est écrit dans AX/DX:AX
IN | In put from Port | Lit depuis un port | La destination est le registre AL/AX
INC | Inc rement by 1 | Incrémente un entier | Ajoute un à l'opérande
INT | Call to Int errupt Procedure | Appelle l'interruption identifiée par l'opérande |
INTO | Call to Int errupt Procedure if O verflow | Appelle l'interruption de débordement si le drapeau de débordement est à 1 |
IRET | I nterrupt Ret urn | Revient d'une interruption |
Jcc | J ump if Condition Is Met | Saute si une condition est vérifiée | Mnémoniques : JA, JAE, JB, JBE, JC, JCXZ, JE, JG, JGE, JL, JLE, JNA, JNAE, JNB, JNBE, JNC, JNE, JNG, JNGE, JNL, JNLE, JNO, JNP, JNS, JNZ, JO, JP, JPE, JPO, JS, JZ
JMP | J u mp | Saute (inconditionnel) |
LAHF | L oad F lags into AH | Copie le registre des drapeaux dans le registre AH |
LDS | L oad DS with Far Pointer | Charge un pointeur de type far (lointain) dans le registre DS (segment) et l'opérande (décalage) |
LEA | L oad E ffective A ddress | Charge l'adresse du second opérande dans le premier |
LES | L oad ES with Far Pointer | Charge un pointeur de type far (lointain) dans le registre ES (segment) et l'opérande (décalage) |
LOCK | Assert LOCK # Signal Prefix | Préfixe verrouillant les bus pour les prochaines instructions | Utilisé pour les plateformes multiprocesseurs.
LODSzz | Lo a d S tring | Copie un octet/mot d'une chaine | La destination est le registre AL/AX. Mnémoniques : LODS, LODSB, LODSW
LOOPcc | Loop According to Counter | Décrémente le compteur et saute si une condition est vérifiée | Le compteur est le registre CX. Mnémoniques : LOOP, LOOPE, LOOPNE, LOOPNZ, LOOPZ
MOV | Mov e | Copie le second opérande dans le premier |
MOVSzz | Mov e from S tring to String | Copie un octet/mot d'une chaîne vers une autre | Mnémoniques : MOVS, MOVSB, MOVSW
MUL | Unsigned Mul tiply | Multiplie par un entier non signé | Le facteur est le registre AL/AX et le produit est écrit dans AX/DX:AX
NEG | Two's Complement Neg ation | Calcule l'opposé de l'opérande (négation par complément à deux ) | Le résultat remplace l'opérande
NOP | N o Op eration | Ne fait rien |
NOT | One's Complement Negation | Effectue un NON logique (négation par complément à un ) | Le résultat remplace l'opérande
OR | Logical Inclusive OR | Effectue un OU logique inclusif des opérandes | Le résultat remplace le premier opérande
OUT | Out put to Port | Écrit vers un port | La source est le registre AX/AL
POP | Pop a Value from the Stack | Dépile vers le registre opérande |
POPF | Pop F lags Register from the Stack | Dépile vers le registre des drapeaux |
PUSH | Push a Value onto the Stack | Empile le registre opérande |
PUSHF | Push F lags Register onto the Stack | Empile le registre des drapeaux |
RCL | R otate through C arry L eft | Effectue une rotation à gauche via par le drapeau de retenue |
RCR | R otate through C arry R ight | Effectue une rotation à droite via par le drapeau de retenue |
REPcc | Rep eat String Operation Prefix | Préfixe de répétition d'une opération sur une chaîne | Mnémoniques : REP, REPE, REPNE, REPNZ, REPZ
RET | Ret urn from Procedure | Revient d'une procédure |
ROL | Ro tate L eft | Effectue une rotation vers la gauche |
ROR | Ro tate R ight | Effectue une rotation vers la droite |
SAHF | S tore AH into F lags | Copie le registre AH dans le registre des drapeaux |
SAL | S hift A rithmectically L eft | Décalage arithmétique vers la gauche | Même instruction que SHL
SAR | S hift A rithmectically R ight | Décalage arithmétique vers la droite | À ne pas confondre avec SHR
SBB | Integer S u b traction with B orrow | Soustrait deux entiers, ajoute le drapeau de retenue | Le résultat remplace le premier opérande
SCASzz | Sca n S tring | Compare un octet/mot d'une chaine | La référence est le registre AL/AX. Mnémoniques : SCAS, SCASB, SCASW
SHL | Sh ift L eft | Décalage logique à gauche | Même instruction que SAL
SHR | Sh ift R ight | Décalage logique à droite | À ne pas confondre avec SAR
STC | S e t C arry Flag | Met le drapeau de retenue à un |
STD | S e t D irection Flag | Met le drapeau de direction à un |
STI | S e t I nterrupt Flag | Met le drapeau d'interruption à un |
STOSzz | Sto re in S tring | Copie dans un octet/mot d'une chaine | La source est le registre AL/AX. Mnémoniques : STOS, STOSB, STOSW
SUB | Sub tract | Soustrait deux entiers | Le résultat remplace le premier opérande
TEST | Logical Compare | Compare deux opérandes (de façon logique) | Positionne les drapeaux en fonction du résultat du ET logique entre les opérandes
WAIT | Wait | Attend tant que la broche BUSY du processeur est inactive | Utilisé avec l' unité de calcul en virgule flottante
XCHG | E xch an g e | Permute le contenu des deux opérandes |
XLAT | Table Look-up Translation | Translation de donnée entre des tables |
XOR | Logical E x clusive OR | Effectue un OU logique exclusif des opérandes | Le résultat remplace le premier opérande

Ajouts aux processeurs 80186/80188

Nom | Sens | Remarques
BOUND | Check array index against bounds | raises software interrupt 5 if test fails
ENTER | Enter stack frame | equivalent to PUSH BP MOV BP, SP
INS, INSB, INSW | Input from port to string | equivalent to IN (E)AX, DX MOV ES:[(E)DI], (E)AX
LEAVE | Leave stack frame | equivalent to MOV SP, BP POP BP
OUTS, OUTSB, OUTSW | Out put string to port | equivalent to MOV (E)AX, DS:[(E)SI] OUT DX, (E)AX
POPA | Pop all general purpose registers from stack | equivalent to POP DI, SI, BP, SP, BX, DX, CX, AX
PUSHA | Push all general purpose registers onto stack | equivalent to PUSH DI, SI, BP, SP, BX, DX, CX, AX

Ajouts aux processeurs 80286

Nom | Sens | Remarques
ARPL | Adjust RPL field of selector |
CLTS | Clear task- switched flag in register CR0 |
LAR | Load access rights byte |
LGDT | Load global descriptor table |
LIDT | Load interrupt descriptor table |
LLDT | Load local descriptor table |
LMSW | Load machine status word |
LOADALL | Load all CPU registers, including internal ones such as GDT | Undocumented, (80)286 and 386 only
LSL | Load segment limit |
LTR | Load task register |
SGDT | Store global descriptor table |
SIDT | Store interrupt descriptor table |
SLDT | Store local descriptor table |
SMSW | Store machine status word |
STR | Store task register |
VERR | Verify a segment for reading |
VERW | Verify a segment for writing

Ajouts aux processeurs 80386

Nom | Sens | Remarques
BSF | Bit scan forward |
BSR | Bit scan reverse |
BT | Bit test |
BTC | Bit test and complement |
BTR | Bit test and reset |
BTS | Bit test and set |
CDQ | Convert double-word to quad-word | Sign-extends EAX into EDX, forming the quad-word EDX:EAX. Since (I)DIV uses EDX:EAX as its input, CDQ must be called after setting EAX if EDX is not manually initialized (as in 64/32 division) before (I)DIV.
CMPSD | Compare string double-word | Compares ES:[(E)DI] with DS:[SI]
CWDE | Convert word to double-word | Unlike CWD, CWDE sign-extends AX to EAX instead of AX to DX:AX
INSB, INSW, INSD | Input from port to string with explicit size | same as INS
IRETx | Interrupt return; D suffix means 32-bit return, F suffix means do not generate epilogue code (i.e. LEAVE instruction) | Use IRETD rather than IRET in 32-bit situations
JCXZ, JECXZ | Jump if register (E)CX is zero |
LFS, LGS | Load far pointer |
LSS | Load stack segment |
LODSW, LODSD | Load string | can be prefixed with REP
LOOPW, LOOPD | Loop | Loop; counter register is (E)CX is counter
LOOPEW, LOOPED | Loop while equal |
LOOPZW, LOOPZD | Loop while zero |
LOOPNEW, LOOPNED | Loop while not equal |
LOOPNZW, LOOPNZD | Loop while not zero |
MOVSW, MOVSD | Move data from string to string |
MOVSX | Move with sign-extend |
MOVZX | Move with zero-extend |
POPAD | Pop all double-word (32-bit) registers from stack | Does not pop register ESP off of stack
POPFD | Pop data into EFLAGS register |
PUSHAD | Push all double-word (32-bit registers) onto stack |
PUSHFD | Push EFLAGS register onto stack |
SCASD | Scan string data dbouble-word |
SETA, SETAE, SETB, SETBE, SETC, SETE, SETG, SETGE, SETL, SETLE, SETNA, SETNAE, SETNB, SETNBE, SETNC, SETNE, SETNG, SETNGE, SETNL, SETNLE, SETNO, SETNP, SETNS, SETNZ, SETO, SETP, SETPE, SETPO, SETS, SETZ | Set byte to one on condition |
SHLD | Shift left double-word |
SHRD | Shift right double-word |
STOSx | Store string |

Ajouts aux processeurs 80486

Nom | Sens | Remarques
BSWAP | Byte Swap | Only works for 32 bit registers.
CMPXCHG | CoMPare and eXCHanGe |
CPUID | CPU IDentification |
INVD | Invalidate Internal Caches |
INVLPG | Invalidate TLB Entry |
WBINVD | Write Back and Invalidate Cache |
XADD | Exchange and Add |

Ajouts aux processeurs Pentium

Nom | Sens | Remarques
CMPXCHG8B | CoMPare and eXCHanGe 8 bytes |
RDMSR | ReaD from Model-Specific Register |
RDTSC | ReaD Time Stamp Counter |
WRMSR | WRite to Model-Specific Register |

Ajouts aux processeurs Pentium MMX

RDPMC: RDPMC à été introduit avec le processeur Pentium Pro et le processeur Pentium avec la technologie MMX.

Ajouts aux processeurs Pentium Pro

Conditioné à MOV: CMOVA, CMOVAE, CMOVB, CMOVBE, CMOVC, CMOVE, CMOVG, CMOVGE, CMOVL, CMOVLE, CMOVNA, CMOVNAE, CMOVNB, CMOVNBE, CMOVNC, CMOVNE, CMOVNG, CMOVNGE, CMOVNL, CMOVNLE, CMOVNO, CMOVNP, CMOVNS, CMOVNZ, CMOVO, CMOVP, CMOVPE, CMOVPO, CMOVS, CMOVZ, SYSENTER (SYStem call ENTER), SYSEXIT (SYStem call EXIT), RDPMC, UD2.

Ajouts aux processeurs AMD K6-2

SYSCALL, SYSRET (Fonctionnalité spécifique au processeur AMD équivalente à l'instruction SYSENTER et SYSEXIT)

Ajouts aux processeurs SSE

MASKMOVQ, MOVNTPS, MOVNTQ, PREFETCH0, PREFETCH1, PREFETCH2, PREFETCHNTA, SFENCE (Mise en cache et ordonnancement mémoire Memory Ordering)

Ajouts aux processeurs SSE2

CLFLUSH, LFENCE, MASKMOVDQU, MFENCE, MOVNTDQ, MOVNTI, MOVNTPD, PAUSE (for Cacheability)

Ajouts aux processeurs SSE3

LDDQU (pour l'encodage vidéo)
MONITOR, MWAIT (Pour la synchronisation des threads, pour les processeurs supportant l'Hyper-Threading et les processeurs dual-core comme le Core 2, le Phenom, etc.)

Ajouts aux processeurs Intel VT

VMPTRLD, VMPTRST, VMCLEAR, VMREAD, VMWRITE, VMCALL, VMLAUNCH, VMRESUME, VMXOFF, VMXON

Ajouts aux processeurs AMD-V

CLGI, SKINIT, STGI, VMLOAD, VMMCALL, VMRUN, VMSAVE (SVM instructions of AMD-V)

Ajouts aux processeurs x86-64

CMPXCHG16B (CoMPaRe and eXCHanGe 16 bytes)

Ajouts aux processeurs SSE4a

LZCNT, POPCNT (POPulation CouNT) - advanced bit manipulation

x87 : Instructions d'unité de calcul en virgule flottante

Instructions du processeur x87 original

F2XM1, FABS, FADD, FADDP, FBLD, FBSTP, FCHS, FCLEX, FCOM, FCOMP, FCOMPP, FDECSTP, FDISI, FDIV, FDIVP, FDIVR, FDIVRP, FENI, FFREE, FIADD, FICOM, FICOMP, FIDIV, FIDIVR, FILD, FIMUL, FINCSTP, FINIT, FIST, FISTP, FISUB, FISUBR, FLD, FLD1, FLDCW, FLDENV, FLDENVW, FLDL2E, FLDL2T, FLDLG2, FLDLN2, FLDPI, FLDZ, FMUL, FMULP, FNCLEX, FNDISI, FNENI, FNINIT, FNOP, FNSAVE, FNSAVEW, FNSTCW, FNSTENV, FNSTENVW, FNSTSW, FPATAN, FPREM, FPTAN, FRNDINT, FRSTOR, FRSTORW, FSAVE, FSAVEW, FSCALE, FSQRT, FST, FSTCW, FSTENV, FSTENVW, FSTP, FSTSW, FSUB, FSUBP, FSUBR, FSUBRP, FTST, FWAIT, FXAM, FXCH, FXTRACT, FYL2X, FYL2XP1

Ajouts aux processeurs spécifiques

Ajouts aux processeurs 80287

FSETPM

Ajouts aux processeurs 80387

FCOS, FLDENVD, FNSAVED, FNSTENVD, FPREM1, FRSTORD, FSAVED, FSIN, FSINCOS, FSTENVD, FUCOM, FUCOMP, FUCOMPP

Ajouts aux Pentium Pro

FCMOV variants: FCMOVB, FCMOVBE, FCMOVE, FCMOVNB, FCMOVNBE, FCMOVNE, FCMOVNU, FCMOVU
FCOMI variants: FCOMI, FCOMIP, FUCOMI, FUCOMIP

Ajouts aux processeurs SSE

FXRSTOR, FXSAVE ( Also supported on later Pentium IIs, though they do not contain SSE support)

Ajouts aux processeurs SSE3

FISTTP (conversion x87 vers entier)

Instructions SIMD

Instructions MMX
Ajouts aux processeurs Pentium MMXS

EMMS, MOVD, MOVQ, PACKSSDW, PACKSSWB, PACKUSWB, PADDB, PADDD, PADDSB, PADDSW, PADDUSB, PADDUSW, PADDW, PAND, PANDN, PCMPEQB, PCMPEQD, PCMPEQW, PCMPGTB, PCMPGTD, PCMPGTW, PMADDWD, PMULHW, PMULLW, POR, PSLLD, PSLLQ, PSLLW, PSRAD, PSRAW, PSRLD, PSRLQ, PSRLW, PSUBB, PSUBD, PSUBSB, PSUBSW, PSUBUSB, PSUBUSW, PSUBW, PUNPCKHBW, PUNPCKHDQ, PUNPCKHWD, PUNPCKLBW, PUNPCKLDQ, PUNPCKLWD, PXOR

Instructions MMX+
Ajouts aux processeurs Athlon

Same as the SSE SIMD Integer Instructions which operated on MMX registers.

Instructions EMMX
Ajouts aux processeurs 6x86MX de Cyrix

PAVEB, PADDSIW, PMAGW, PDISTIB, PSUBSIW, PMVZB, PMULHRW, PMVNZB, PMVLZB, PMVGEZB, PMULHRIW, PMACHRIW

Instructions 3DNOW!
Ajouts aux processeurs K6-2

FEMMS, PAVGUSB, PF2ID, PFACC, PFADD, PFCMPEQ, PFCMPGE, PFCMPGT, PFMAX, PFMIN, PFMUL, PFRCP, PFRCPIT1, PFRCPIT2, PFRSQIT1, PFRSQRT, PFSUB, PFSUBR, PI2FD, PMULHRW, PREFETCH, PREFETCHW

Instructions 3DNOW!+
Ajouts aux processeurs Athlon

PF2IW, PFNACC, PFPNACC, PI2FW, PSWAPD

Ajouts aux processeurs Geode GX

PFRSQRTV, PFRCPV

Instructions SSE

Voir aussi les instructions sur les entiers ajoutées au processeur Pentium III.

Ajouts Instructions SSE SIMD en virgule flottante

ADDPS, ADDSS, CMPPS, CMPSS, COMISS, CVTPI2PS, CVTPS2PI, CVTSI2SS, CVTSS2SI, CVTTPS2PI, CVTTSS2SI, DIVPS, DIVSS, LDMXCSR, MAXPS, MAXSS, MINPS, MINSS, MOVAPS, MOVHLPS, MOVHPS, MOVLHPS, MOVLPS, MOVMSKPS, MOVNTPS, MOVSS, MOVUPS, MULPS, MULSS, RCPPS, RCPSS, RSQRTPS, RSQRTSS, SHUFPS, SQRTPS, SQRTSS, STMXCSR, SUBPS, SUBSS, UCOMISS, UNPCKHPS, UNPCKLPS

SSE SIMD Integer Instructions

ANDNPS, ANDPS, ORPS, PAVGB, PAVGW, PEXTRW, PINSRW, PMAXSW, PMAXUB, PMINSW, PMINUB, PMOVMSKB, PMULHUW, PSADBW, PSHUFW, XORPS

Instructions SSE2

Ajouts aux processeurs Pentium 4.

SSE2 SIMD Floating-Point Instructions

ADDPD, ADDSD, ANDNPD, ANDPD, CMPPD, CMPSD*, COMISD, CVTDQ2PD, CVTDQ2PS, CVTPD2DQ, CVTPD2PI, CVTPD2PS, CVTPI2PD, CVTPS2DQ, CVTPS2PD, CVTSD2SI, CVTSD2SS, CVTSI2SD, CVTSS2SD, CVTTPD2DQ, CVTTPD2PI, CVTPS2DQ, CVTTSD2SI, DIVPD, DIVSD, MAXPD, MAXSD, MINPD, MINSD, MOVAPD, MOVHPD, MOVLPD, MOVMSKPD, MOVSD*, MOVUPD, MULPD, MULSD, ORPD, SHUFPD, SQRTPD, SQRTSD, SUBPD, SUBSD, UCOMISD, UNPCKHPD, UNPCKLPD, XORPD

SSE2 SIMD Integer Instructions

MOVDQ2Q, MOVDQA, MOVDQU, MOVQ2DQ, PADDQ, PSUBQ, PMULUDQ, PSHUFHW, PSHUFLW, PSHUFD, PSLLDQ, PSRLDQ, PUNPCKHQDQ, PUNPCKLQDQ

Instructions SSE3

Ajouts au processeur Pentium 4 supportant le SSE3

SSE3 SIMD Floating-Point Instructions

  • ADDSUBPD, ADDSUBPS (for Complex Arithmetic)
  • HADDPD, HADDPS, HSUBPD, HSUBPS (for Graphics)
  • MOVDDUP, MOVSHDUP, MOVSLDUP (for Complex Arithmetic)

Instructions SSSE3

Ajouts aux processeurs Core 2 et Xeon 5100 serie

PSIGNW, PSIGND, PSIGNB, PSHUFB, PMULHRSW, PMADDUBSW, PHSUBW, PHSUBSW, PHSUBD, PHADDW, PHADDSW, PHADDD, PALIGNR, PABSW, PABSD, PABSB

sInstructions SSE4

SSE 4.1

Ajouts aux processeurs Core 2 x9000 series

MPSADBW, PHMINPOSUW, PMULLD, PMULDQ, DPPS, DPPD, BLENDPS, BLENDPD, BLENDVPS, BLENDVPD, PBLENDVB, PBLENDW, PMINSB, PMAXSB, PMINUW, PMAXUW, PMINUD, PMAXUD, PMINSD, PMAXSD, ROUNDPS, ROUNDSS, ROUNDPD, ROUNDSD, INSERTPS, PINSRB, PINSRD/PINSRQ, EXTRACTPS, PEXTRB, PEXTRW, PEXTRD/PEXTRQ, PMOVSXBW, PMOVZXBW, PMOVSXBD, PMOVZXBD, PMOVSXBQ, PMOVZXBQ, PMOVSXWD, PMOVZXWD, PMOVSXWQ, PMOVZXWQ, PMOVSXDQ, PMOVZXDQ, PTEST, PCMPEQQ, PACKUSDW, MOVNTDQA

SSE 4.a

Ajouts aux processeurs Phenom

EXTRQ/INSERTQ, MOVNTSD/MOVNTSS

SSE 4.2

Ajouts aux processeurs Nehalem

CRC32, PCMPESTRI, PCMPESTRM, PCMPISTRI, PCMPISTRM, PCMPGTQ

Références

  • The 8086 / 80286 / 80386 / 80486 Instruction Set
  • Free IA-32 and x86-64 documentation , par Intel
  • Netwide Assembler x86 Instruction Reference (de Netwide Assembler )
  • x86 Instruction Set Reference


C-ASM version 1.0f

 C-ASM version 1.0f : LE cours d'initiation à l'assembleur PC !
 ~~~~~~~~~~~~~~~~~~

    Pour apprécier à juste mesure les cours d'initiation à l'assembleur qu'il
 y a sur cette disquette, je vous ai mis un utilitaire de navigation sous DOS
 qui se nomme : 'LIST'.

    Je vous souhaite une bonne lecture de 'C-ASM1.TXT' et 'C-ASM2.TXT' et je
 pense que vous serez très heureux de me retrouver pour la suite de ce cours
 dans la version 1.1s (cf détails pour se procurer cette version dans le 2ième
 chapitre de l'initiation).

    Mon adresse :        Philippe HENROTTE
    ~~~~~~~~~~~     5 Bis rue de PLANCHEPALEUIL
                            63200 RIOM


LISEZ.TXT

       ÉÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
       º  BON DE COMMANDE DE LA VERSION ENREGISTREE DE C-ASM  º
       ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ¼

    DESCRIPTION DE LA VERSION 1.1s
    ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    La version de C-ASM que vous avez actuellement entre les mains n'est
 qu'un extrait de ce que contient la version complète, c'est juste pour
 vous donner un léger aper‡u de cette initiation à l'assembleur PC.

    Voici le contenu de la disquette HD compactée de C-ASM v1.1s :
    øøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøøø

    * 10 chapitres sur l'apprentissage progressif de l'ASM 80x86,

    * une multitude de docs techniques sur les interruptions, ports,
      vidéo... pour faire le bonheur de tous les programmeurs PC !,

    * encore plus de sources ASM que sur cette disquette,

    * des utilitaires destinés aux programmeurs,

    * des surprises diverses...

    Cette version 1.1s nécessite une disquette HD ou environ 2,5 Mo sur votre
 disque-dur, si vous installez tous les ingrédients de cette disquette.

    Mais oui, "C-ASM" est LA référence dans ce domaine pour les programmeurs
 fran‡ais !!!

    COMMENT SE PROCURER CETTE VERSION 1.1s ?
    ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Pour recevoir la version complète de C-ASM, veuillez découper le coupon
 réponse ci-dessous (ou le recopier sur papier libre), ceci afin de me
 permettre de vous affecter un numéro d'enregistrement.

    La contribution pour recevoir la version 1.1s est de 30 FF et 1 disquette
 Haute Densité pour la copie à vous retourner. Etant donné la mine d'or que
 recèle la disquette de C-ASM v1.1s en matière d'informations sur le hardware
 et la programmation en ASM sur PC, le prix de 30 FF n'a absolument rien de
 démesuré et bien au contraire.

    ENVOYEZ VOTRE CHEQUE ET LA DISQUETTE HD A :
    ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

                        Philippe HENROTTE
                     5 Bis rue de PLANCHEPALEUIL
                           63200 RIOM

    (réponse sous 48H-Chrono après réception de votre courrier)

  ÉÍ COUPON REPONSE ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ»
  º                                                                    º
  º      Date : ___/___/___                                            º
  º                                                                    º
  º       Nom : __________________   Prénom : __________________       º
  º                                                                    º
  º   Adresse : ________________________________________________       º
  º                                                                    º
  º     Ville : _______________________________ C.P. : _________       º
  º                                                                    º
  º                                                                    º
  º Comment avez eu C-ASM 1.0f ? :  ____________________________       º
  º                                                                    º
  º                                 ____________________________       º
  º                                                                    º
  º                                 ____________________________       º
  º                                                                    º
  ÈÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ¼

    Merci de votre confiance et je vous souhaite un bonne initiation à l'ASM
 avec mon shareware C-ASM v1.1s qui renvoie la plupart des livres commerciaux
 aux oubliettes. En France, C-ASM v1.1s a été élu comme LA référence dans ce
 domaine, après plusieurs démonstrations dans des salons informatiques.

                              A bient“t !,

                               P. HENROTTE :-)


INITIATION A L'ASSEMBLEUR 80x86 "chapitre 1"

                     ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
                     Þ INITIATION A L'ASSEMBLEUR 80x86 Ý
                     ÞÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÝ
                     Þ    CHAPITRE #1    SERIE 1     Ý
                     ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
                      (c) P. HENROTTE  v 1.0f {02/1995}

     Vous avez décidé d'apprendre le langage Assembleur PC d'une maniŠre assez
  rapide et efficace. Je vous propose donc une série de dix chapitres qui sont
  en fait l'essentiel des connaissances à acquérir pour faire ses premiers pas
  en ASM 80x86.

     Pour ce faire, l'ensemble de ce cours est axé sur le microprocesseur 8086
  mais rassurez vous, les possesseurs de 386/486/Pentium..., auront le droit à
  quelques informations supplémentaires afin d'exploiter au mieux leur machine
  adorée.

     J'espŠre que vous aurez le courage de lire cette initiation jusqu'au bout
  car j'admets que c'est un peu lourd à digérer au premier abord !... mais bon
  l'ASM n'est pas fait non plus pour les "pantouflards" !.. De plus, il faudra
  ˆtre trŠs méthodique et sérieux, sinon vous pouvez tout de suite retourner à
  vos occupations habituelles.

     On se penche tout de suite et sans plus attendre, sur le premier chapitre
  basé sur la description des divers registres du microprocesseur "INTEL"(tm).
  Et on se contentera de ce sujet pour l'instant, histoire que vous assimiliez
  bien les informations de ce chapitre.

  I - LES REGISTRES : Cellules grises d'un ordinateur
  ßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßß

     Les ordinateurs ont cette particularité, de posséder une architecture qui
  gravite autour de microprocesseurs. Ces derniers renferment une multitude de
  composants électroniques, dont ces fameux registres. Les registres étant une
  partie indispensable au bon fonctionnement d'un ordinateur, cela explique ce
  paragraphe consacré à leur étude détaillée. Il faut noter qu'il en existe de
  plusieurs types, dont vous pouvez vois la classification ci-dessous.

     Nous avons des registres : - de données,
                                - d'adresses,
                                - de segment,
                                - et l'indicateur.

     Pour le moment retenez juste qu'il existe 4 types de registres différents
  dont je vais faire la description de fa‡on assez simple. Pour ne pas sombrer
  dans les méandres du langage Assembleur, lisez bien tous les détails.

  1 - Les registres de données
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

     AX = Accumulateur pour les multiplications et divisions sur 16 bits ainsi
          que pour les entrées/sorties.

          AX = Registre de 16 bits qui se décompose en 2 registres sur
               8 bits que sont AH (poids fort) et AL (poids faible).

                                 AX
                               ÚÄÁÁÄ¿
                            ÚÄÄÁÄ¿ÚÄÁÄÄ¿
                              AH    AL

     BX = Registre de base (il contient alors une adresse) dans le cas d'un
          adressage basé.

          BX = BH + BL

                                 BX
                               ÚÄÁÁÄ¿
                            ÚÄÄÁÄ¿ÚÄÁÄÄ¿
                              BH    BL

     CX = Registre qui sert de compteur dans le cas de boucles ou d'opérations
          sur les chaŒnes de caractŠres.

          CX = CH + CL

                                 CX
                               ÚÄÁÁÄ¿
                            ÚÄÄÁÄ¿ÚÄÁÄÄ¿
                              CH    CL

     DX = Il sert d'extension de l'accumulateur dans le cas de multiplications
          ou de divisions sur 16 bits. Il est aussi utile, dans les opérations
          d'entrées/sorties.

          DX = DH + DL

                                 DX
                               ÚÄÁÁÄ¿
                            ÚÄÄÁÄ¿ÚÄÁÄÄ¿
                              DH    DL

     En résumé, pour bien retenir la contenu du paragraphe ci-dessus, voici ma
  méthode qui consiste à associer le r“le de chaque registre à un mot clé :

                        AX = ACCUMULATEUR

                        BX = BASE

                        CX = COMPTEUR

                        DX = DONNEES

     N.B : Pour les possesseurs de 386/486/Pentium+, vous avez la possibilité
     ÍÍÍ   d'avoir des registres 32 bits en ajoutant un "E" (registre étendu)
           devant chaque registre (EAX, EBX...) tout en signalant au début de
           votre programme, le type de machine que vous possédez (Ex : .486).
           Certains des sources ASM qui se trouvent sur la disquette pourront
           vous aider à mettre en place cette méthode simple mais efficace.

  2 - Les registres d'adresses
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

     SI = Source Index

          Ce registre est en principe utilisé lors de manipulations de chaŒnes
          de caractŠres. Il est associé au registre DS.

     DI = Destination Index.

          Identique à SI, sauf que dans le cas d'opérations sur des chaŒnes de
          caractŠres, il est associé à ES.

     IP = Instruction Pointer.

          Il est associé à CS (CS:IP) pour indiquer la prochaine instruction à
          exécuter.

     BP = Base Pointer.

          Il est associé à SS (SS:BP) pour accéder aux données de la pile lors
          d'appels de sous programmes.

     SP = Stack Pointer.

          Là c'est pareil, on le retrouve associé à SS (SS:SP) afin d'indiquer
          le dernier élément de la pile.

     Comme dans le paragraphe précédent, la méthode pour tout retenir est :

                        SI = SOURCE

                        DI = DESTINATION

                        IP = INSTRUCTION

                        BP = POINTEUR DE BASE

                        SP = POINTEUR DE PILE

  3 - Les registres de segment
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

     CS = Code Segment.

          L'adresse qu'il contient correspond à l'emplacement des instructions
          du programme.

     DS = Data Segment.

          Idem que CS, mais pour les données du programme.

     SS = Stack Segment.

          Le segment dont l'adresse est contenue dans ce registre vous donnera
          la pile.

     ES = Extra Segment.

          Adresse d'un segment supplémentaire pour les données.

  4 - Le registre indicateur
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

     C'est juste un registre de 16 bits, dont 9 seulement sont utilisés !. Les
     bits inutilisés sont toujours à zéro.

         15 14 13 12 11 10  9  8  7  6  5  4  3  2  1  0   <ÄÄ Bits
        ÚÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄÂÄÄ¿
        ³00³00³00³00³OF³DF³IF³TF³SF³ZF³00³AF³00³PF³00³CF³  <ÄÄ Flags
        ÀÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÁÄÄÙ

     CF = "Carry" ou indicateur de retenue.

          Lors d'une addition ou d'une soustraction s'il y a une retenue, elle
          est enregistrée dans cet indicateur.

     PF = "Parity" ou indicateur de parité.

          PF donne la parité des 8 bits de poids faible du résultat. Et, si le
          nombre de bits à 1 est nul ou pair, PF prend la valeur 1 et s'il est
          impair, PF prend la valeur zéro.

     AF = "Auxiliary carry" ou indicateur auxiliaire de retenue.

          Il est similaire au bit CF, sauf qu'il s'occupe de la présence d'une
          éventuelle retenue "débordant" des 4 bits de poids faible.

     ZF = "Zero" ou Indicateur zéro.

          Si le résultat d'une opération est 0, ZF passe à 1, sinon il passe à
          zéro.

     SF = "Sign" ou Indicateur de signe.

          Le bit de poids fort du résultat est recopié dans SF.

     TF = "Trap" ou indicateur de pas-à-pas.

          S'il est à 1, alors il permet de faire travailler le microprocesseur
          instruction par instruction. Ceci est trŠs utile pour débugger votre
          programme.

     IF = "Interrupt enable" ou indicateur d'autorisation d'interruptions.

          Cet indicateur peut autoriser ou inhiber, les interruptions qui sont
          extérieures au microprocesseur (1 = autorise ; 0 = interdit).

     DF = Indicateur de direction.

          Il permet l'incrémentation et décrémentation automatique du registre
          index, aprŠs l'exécution d'une instruction de chaŒne de caractŠres.

     OF = "Overflow" ou indicateur de débordement.

          Il est positionné à 1, si le résultat de l'addition sur 2 nombres de
          mˆme signe ou de la différence de 2 nombres de signes différents, ne
          peut ˆtre codé sur le nombre de bits autorisé.

 II - CONCLUSION
 ßßßßßßßßßßßßßßß

     Voilà, ce premier chapitre est terminé et je suis s–r que vous n'avez pas
  tout compris à la subtilité des registres, mais ce n'est pas trop grave pour
  le moment, car vous ne faites que débuter l'ASM. Attendez d'en savoir un peu
  plus sur l'Assembleur et de commencer à faire vos premiŠres petites routines
  pour maŒtriser à fond ce sujet.

     Pour l'instant retenez que les registres sont le coeur du microprocesseur
  et par conséquent, c'est ce qui entretien la vie de votre machine. Le æ80x86
  possŠde peu de registres, c'est pour cela qu'il faudra savoir les utiliser à
  la perfection. Mais n'ayez crainte, ce cours vous apportera toutes les bases
  essentielles de la connaissance en ASM !...

     Le prochain chapitre abordera le thŠme sur les modes d'adressage, je sais
  que ce sujet va vous prendre la tˆte au possible, mais bon, c'est le passage
  obligé avant d'en arriver aux réjouissances des prochains chapitres... alors
  faites le plein de courage, aprŠs je vous assure le plaisir ultime !!!. A+


INITIATION A L'ASSEMBLEUR 80x86 "chapitre 2"

                     ÞßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßßÝ
                     Þ INITIATION A L'ASSEMBLEUR 80x86 Ý
                     ÞÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÝ
                     Þ    CHAPITRE #2    SERIE 1     Ý
                     ÞÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÝ
                      (c) P. HENROTTE  v 1.0f {02/1995}

    Avant de lire ce cours, j'espère que vous avez fait l'effort de comprendre
 le chapitre sur les registres ?!, sinon retour à la case départ avant que je
 me fîche !. C'est inutile de survoler les chapitres pour apprendre plus vite,
 car je fais un cours progressif et relativement rapide pour que vous ne soyez
 pas tenté par cette idée. Bien s–r, si vous maŒtrisez déjà parfaitement tel
 ou tel sujet, je vous permets de passer assez rapidement dessus mais je vous
 déconseille de faire l'impasse totale. Je peux vous apporter une information
 capitale dont vous n'avez pas connaissance... alors méfiance !

  I - MODES D'ADRESSAGE
  ßßßßßßßßßßßßßßßßßßßßß

    Il existe plusieurs manières de calculer l'adresse de l'opérande et pour
 tout vous dire, voici les différents modes adressage :

        1 - Registre
        2 - Immédiat
        3 - Direct
        4 - Basé
        5 - Indexé
        6 - Basé et indexé
        7 - Sans déplacement ou implicite
        8 - Adressage relatif

    En principe je n'ai oublié personne ?!... alors nous pouvons passer à une
 explication plus détaillée de chacun de ces modes. C'est parti !

  1 - Adressage registre
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    On ne peut pas vraiment parler d'adressage puisque l'opérande est un
 registre et non une "case mémoire".

    Ex : L'instruction MOV AX,CX  (On reverra plus tard le mécanisme de
    ÍÍ   MOV) copie le contenu de CX dans AX. CX qui est l'opérande,
         étant un registre on parle donc d'adressage registre, c'est tout
         ce qu'il y a comprendre.

  2 - Adressage immédiat
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Là non plus ce n'est pas vraiment un adressage, car en utilisant ce mode
 on indique la valeur de l'opérande et non son adresse.

    Ex : MOV CX,256 met la valeur décimale 256 dans CX.
    ÍÍ

  3 - Adressage direct
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Ici, le principe est à peine plus compliqué, car il s'agit d'indiquer
 l'adresse effective, c'est à dire le nombre d'octets qui sépare l'opérande
 du début du programme (N.B : attention, le programme ne commence rarement à
 l'adresse 0, et donc l'adresse effective ne correspond pas obligatoirement
 à l'adresse physique !. Mais vous comprendrez mieux lorsqu'on commencera
 l'étude d'exemples concrets).

    Ex : MOV AX,ADR transfère dans AX les 16 bits qui ont en mémoire
    ÍÍ   l'adresse de ADR (adresse symbolique).

                                              ÚÄ MOV AX,ADR Ä¿    [ADR] Ä¿
                                                                         ³
    Adresse physique   0 1 ... 31 32    ...    109 110 111 112 ... 245   =
    ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ   ³
    Adresse effective           0  1    ...     12  13  14  15 ... 100 <ÄÙ

    On imagine que le début du programme commence à l'adresse physique 31
 (Soit une adresse effective égale à 0), que l'instruction MOV AX,ADR se
 situe aux adresses [109,110,111,112] et que 245 (en décimal) correspond
 à l'adresse physique de ADR. On en déduit que c'est le nombre 100 qui sera
 codé pour désigner l'adresse de l'opérande.

  4 - Adressage basé
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Ce type d'adressage utilise un des deux registres de base : BX et BP.

    Syntaxe :    [BX]
    ÍÍÍÍÍÍÍ   ou [BX] [déplacement]     Ex : [BX] [100]
              ou [BX] + déplacement          [BX] + 100
              ou [BX + déplacement]          [BX + 100]
              ou déplacement [BX]            ADR [BX]

              ou en utilisant BP à la place de BX.

  5 - Adressage indexé
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Il est semblable à celui du mode d'adressage basé, excepté qu'il prend
 les registres index au lieu des registres de base.

    Les registres SI et DI sont utilisés ainsi que DS.

  6 - Adressage basé et indexé
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Cette fois-ci, on doit spécifier un registre de base et un registre
 d'index. L'adresse effective est obtenue en ajoutant au déplacement le
 contenu des 2 types de registres.

                        ÄÄ¿
    Ex : [BX] [SI] + 1    ³
    ÍÍ   [BX] [SI + 1]    ÃÄÄÄ> pour un déplacement de 1
         [BX + 1] [SI]    ³
                        ÄÄÙ
                        ÄÄ¿
         ADR [BX] [SI]    ÃÄÄÄ> pas de déplacement
         [BX] [SI]        ³
                        ÄÄÙ

  7 - Adressage implicite
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Comme son nom l'indique, il n'y a aucun déplacement !

    Ex : [BX]
    ÍÍ   [BX + SI]
         ...

  8 - Adressage relatif
  ÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍÍ

    Il est utilisé pour les branchements conditionnels. Pour calculer le
 branchement à effectuer, on doit ajouter au compteur ordinal, la valeur du
 déplacement + 2.

    Ex : Si vous exécuter une instruction de saut (JMP) pour retourner à un
    ÍÍ   endroit bien précis de votre programme, on procède ainsi :

           Adresse effective     Instruction
             (en base 10)

                112           DEB: MOV CX,512
                 ³                  ³
                ...                ...
                 ³                  ³
                140               JNZ DEB
                142

           Déplacement = (140 + 2 - 112)
                       = 30

  II - EXEMPLE
  ßßßßßßßßßßßß

          ...
          .DATA

adrlist db 1024 dup (0)                ; Pointeurs sur les fréquences

          ...

          .CODE
          ...

insertion proc near
;-------------------------------------------------------------;
; Ceci est un extrait d'une de mes routines non-optimisées    ;
; Liste des caractères par ordre de fréquences décroissantes. ;
;-------------------------------------------------------------;

          mov  ax,255                  ; Maximum de car. à trier
          mov  bx,ax                   ; 1 car. = 1 word
tri:      cmp  bx,4                    ; Tant que BX > adrlist[1]
          jbe  fintri
          lea  si,[bx+adrlist]

          mov  dx,[si-6]               ; DX=adrlist[BX-1]
rpt:      lea  si,adrlist
          add  si,ax

          cmp  dx,[si-2]               ; DX >= adrlist[AX] ?
          jae  init
          cmp  ax,max                  ; Si dépassement...
          ja   init                    ; on passe à la suite
          cmp  dx,[si-2]               ; DX <= adrlist[AX] ?
          ja   decrnt
          mov  cx,[si-2]               ; Permutation des fréquences
          mov  [si-2],dx
          mov  [si-6],cx

          push dx                      ; Sauvegarde [SI-6]
          mov  dx,[si-8]               ; Permutation des adresses
          mov  cx,[si-4]               ; associées aux fréquences
          mov  [si-4],dx
          mov  [si-8],cx
          pop  dx                      ; Récupère [SI-6]

decrnt:   add  ax,4                    ; Mot suivant
          jmp  short rpt
init:     sub  bx,4                    ; Mot précédent
          mov  ax,bx
          jmp  short tri
fintri:   ret
insertion endp

    Vous avez dans cette routine de tri que je viens de faire assez rapidement
 plusieurs exemples de modes d'adressage. Et si vous prenez votre temps pour
 comprendre le programme ASM, alors ce chapitre ne sera plus qu'une formalité
 pour vous !...

  III - CONCLUSION
  ßßßßßßßßßßßßßßßß

    Je sais que pour le moment toutes ces informations vous semblent trop
 théoriques pour ne pas dire incompréhensibles... cependant je suis obligé de
 vous donner certaines bases avant de parler programmation assembleur. Le
 prochain cours sera bien plus intéressant puisqu'on abordera les principales
 instructions ASM, la pile... et par conséquent les premiers programmes.

    Mais si vous ˆtes intéressé par la suite de ce cours qui se décompose en
 dix chapitres dont vous venez d'en voir les 2 premiers (et de loin les moins
 passionnants !...), il ne vous reste plus qu'à vous procurer la version 1.1s.
 Excusez-moi de vous donner aussi peu d'informations sur ce cours d'assembleur
 mais étant donné le prix minuscule de la licence d'utilisation, vous ne prenez
 aucun risque à me commander la version complète.

    La version 1.1s comprend :
    ÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜ

    - 10 chapitres sur l'apprentissage progressif de l'ASM 80x86

    - plus de sources (et plus intéressantes !)

    - une multitude de docs techniques sur les interruptions, ports, vidéo...
      de quoi faire le bonheur de n'importe quel programmeur PC !!!

    - des utilitaires destinés aux programmeurs

    - des surprises diverses...

    Cette version 1.1s nécessite une disquette HD, environ 2,5 Mo sur votre
 disque-dur, si vous installez tous les ingrédients de cette disquette.

    Mais oui, "C-ASM" (nom de ce soft') et LA référence dans ce domaine pour
 les programmeurs fran‡ais !!!

    Comment se procurer cette version 1.1s ?
    ÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜ

    Etant donné les nombreuses heures que j'ai passé avant de finir l'écriture
 de ces 10 chapitres et du temps qu'il m'a fallu pour dénicher toutes ces docs
 techniques indispensables à ceux qui veulent créer des démos ou jeux... il est
 dans mon droit de vous demander une petite récompense financière.

    C'est pas 100 ni 50 que je vous demande, mais seulement 30 Francs en chèque
 libellé à mon nom + l'envoi d'une disquette HD pour la copie à vous retourner.
 Je m'engage (sauf grève de La Poste) à vous renvoyer sous 48 heures votre disk
 Haute Densité avec cette version 1.1s dessus !... A+

    Envoyez votre chèque de 30 Francs + 1 disk HD + 1 timbre, à :
    ÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜÜ

                              Philippe HENROTTE
                          5 Bis rue de PLANCHEPALEUIL
                                 63200 RIOM
                                   FRANCE


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP1.rtf

CHAPITRE 1 - Éléments de base Vu dans Doc shasm.zip/ASMCHARP.ZIP pour Tasm

Comme vous le savez peut être déjà, l’assembleur est un langage qui travaille directement sur les registres du processeur. Il est donc impératif de bien comprendre l’architecture et le fonctionnement interne des microprocesseurs d’ Intel avant d’entreprendre l’apprentissage du langage. Ce premier cours présentera donc très peu de code source et beaucoup de notions théorique. L’assembleur qui sera utilisé est Turbo Assembler. Une version minimale de ce logiciel, contenant l’assembleur tasm.exe et l’éditeur de liens tlink.exe, est disponible dans la section Téléchargements.

OUTILS NÉCESSAIRES

Pour programmer en assembleur, vous aurez besoin d'un compilateur assembleur. Il existe deux principaux assembleurs sur le marché : Turbo Assembler de Borland et Microsoft Assembler. Vous pouvez également inclure des instructions assembleur directement dans votre code source C. En effet, la commande « asm » signifie au compilateur C que les instructions qui suivent sont de l’assembleur et de les traiter avec son assembleur interne. Remarquez qu’il est aussi possible d’indiquer au compilateur C d’utiliser Turbo Assembler plutôt que son compilateur interne, car ce dernier est beaucoup moins efficace. Dans le cas des compilateur de Borland, l’assembleur interne se nomme BASM.

Également, vous aurez besoin d’une référence en-ligne très complète. Bien qu’elle commence à faire sentir son âge, l’excellente HelpPC constitue une véritable mine d’or d’informations sur la programmation système en assembleur. Il s’agit d’une sorte de d’index de toutes les instructions assembleur, ainsi que des interruptions et des informations sur les processeurs Intel. HelpPC est disponible dans la section Téléchargements.

AVERTISSEMENT

Ce cours ne portera pas sur le set d’instructions de la série Pentium, AMD ou Cyrix. Le contexte du cours se limite aux instructions 80486, bien que l’emphase soit mis sur les registre 16-bits, et ce dans un but pédagogique. Il existe des différences entre MASM et TASM donc il est très possible que les codes sources ne compile pas sous MASM ou sur une version TASM antérieur à la version 5. Ce cours assume que le lecteur connaît les mathématiques Booléennes logiques (ET, OU, etc..).
Finalement, le mode protégé et l’assembleur sous Win32 est définitivement hors de la portée de ce cours.

ASSEMBLAGE D’INSTRUCTIONS

L’assembleur est un langage de programmation de bas niveau utilisant du code mnémonique pour remplacer les opérandes du processeur. Le but premier de l’assembleur est de remplacer des instructions binaires, du langage machine, vers une série de pseudo-instructions qui sont plus significatives qu’une simple série de chiffres binaires. Par exemple :

1101 0110 0111 1110 0000 0010
MOV AX 2

La série de bits ne vous dit probablement rien, mais même si on ne connaît pas l’assembleur, on pourrait déduire, par l’instruction mnémonique « MOV » (move, déplace) que cet commande place la valeur 2 dans le registre AX. Quand nous assemblons des instructions assembleur, le compilateur transforme nos instructions en langage machine, une série de 1 et de 0, qui lui sera interprété par le processeur.

L’ARCHITECTURE D’UN ORDINATEUR

L’architecture d’un ordinateur se compose principalement d’un processeur central, d’un bus, des port d’entrée/sortie et la mémoire. C’est à l’intérieur du microprocesseur central que toutes les traitements se font. Les données et les instructions machines sont stockés en mémoire jusqu'à ce qu’elles soient réclamés par le CPU. Ce dernier peut lire et écrire des données sur des périphériques par l’entremise des ports d’ I/O. Ces transfert s’effectue par le BUS système. Il existe 3 types de BUS sur le PC :

Le BUS de données

Les processeurs 8086 utilise le bus de données pour transporter les données entres les divers composants d’un système. La taille de ce bus varie selon l’architecture. Il se compose généralement de 16, 32 ou 64 lignes. Un bus de 16 lignes ne limite pas le processeurs aux instructions 16 bits : cela signifie simplement qu’il peut recevoir 16 bit par cycle machine. Donc une données sur 32-bit prendrait 2 cycles machine pour être reçu par le processeur. De la même façon, un bus 32-bit peut séparer une instruction 32-bit en 2 de 16, et répartir le transfert sur 2 cycles. Donc, plus le bus est large, plus les données transit rapidement et on peut accéder à plus de mémoire par opération.

Microprocesseur Taille du bus de données
8088 8
8086 16
80286 16
80386 16
80386dx 32
80486 32
80586 64

BUS d’adresses

Le bus de données transporte les données, mais la question demeure : où? Le bus d’adresse contient la réponse à cette question. Afin de faire la différence entre les emplacement en mémoire vive et les périphériques, on désigne chaques adresse par un numéro unique. Quand un programme veut accéder à un périphérique quelconque, il dépose sur le bus de données son adresse et ce périphérique saura qu’on fait appel à lui, car ce numéro est unique. Le nombre de ligne du bus d’adresse détermine le nombre d’adresse possibles. En effet, s’il était composé de 2 lignes, il pourrait composer 2 seuls adresse uniques : 0 et 1. C’est donc en utilisant des puissances de deux qu’on peut calculer l’adressage maximal du bus :

Processeur Taille du bus d’adresse Mémoire adressable
8088 20 1,048,576
8086 20 1,048,576
80286 24 16,777,216
80386sx 24 16,777,216
80386dx 32 4,294,976,296
80486 32 4,294,976,296
80586 32 4,294,976,296

Le BUS de contrôle

Le bus de contrôle est une série de signaux électriques qui contrôle les communications entre le processeur et le reste du système. Il contrôle les lectures et les écritures, et sa structure dépasse le cadre de ce cours.

STRUCTURE D’UN MICROPROCESSEUR INTEL

Au cœur d’un microprocesseur se trouvent les registres, sortes de variables matérielles. Le processeur y charge les données en provenance de la mémoire, les traite à l’aides des instructions machines puis les renvoies dans la mémoire. Ces manipulations sont extrêmement rapides car les registres sont implantés à l’intérieur même du processeur et leur contenu est donc accessible sans l’aide du bus. Il existe plusieurs types de registres :

- registres généraux
- registres de segment
- registres d’offset
- registres d'états

Chacun de ces registres ont une fonctions spécifique, bien qu’il soit possible de les détourner de leurs buts premier. Selon la famille de processeur, ces registres ont une taille de 8, 16 ou 32-bits. Les registres 32-bit sont apparut seulement depuis le 80386 et leurs nom est précédé de la lettre « E ». Voici un tableau avec les différents registres du processeur.

DIAGRAMME INTERNE D’UN PROCESSEUR

LES REGISTRES GÉNÉRAUX

Au nombre de 4, ils ont un usage bien précis bien qu’ils peuvent être utilisé comme zone de stockage intermédiaire :

  • (E)AX Appelé « accumulateur », car il est souvent utilisé pour les opérations arithmétiques.

  • (E) BX Appelé « registre de base », car il est utilisé pour l’adressage basé (voir ci-dessous)

  • (E) CX le « compteur » car il sert pour l’exécution des boucles.

  • (E) DX appelé « data » car il contient les adresses d’I/O quand il faut accéder des données sur le bus.

Notons que chacun de ces registres peut être décomposé en deux registres de 8-bits, qu’on peut accéder individuellement.

LES REGISTRES D’OFFSET

Ces registres contiennent une valeur représentant un offset à combiner à une adresse de segment. Il sont au nombre de 5 :

  • (E) SI - appelé « source index ». Principalement utilisé lors d’opérations sur les chaînes de caractères (comme pointeur sur la source); il est associé au registre de segment DS

  • (E) DI appelé « destination index ». Utilisé lors d’opération sur les chaînes de caractères (comme pointeur sur la destination) et normalement associé au registre de segment DS. Associé à ES s’il traite des chaînes de caractères.

  • (E) IP appelé « instruction pointer ». Ce registre est associé au registre de segment (CS :IP) pour indiquer la prochaine instruction à exécuter. Ce registre ne pourra jamais être modifier directement.

  • (E) BP « Base pointer ». Associé au registre de segment SS (SS :BP) pour accéder aux données de la pile lors d’appels de sous-programme (utilisé pour appelé des fonctions externes)

  • (E) SP Appelé « stack pointer ». Associe au registre de segment SS (SS :BP) pour indiquer le dernier élément de la pile.

LES REGISTRES DE SEGMENT

Ces registres servent à sélectionner différents blocs(segments) de mémoires. Un registre de segment pointe au début d’un segment dans la mémoire. En mode réel, une adresse mémoire est formée d’un segment et d’une offset. C’est par l’utilisation des registres de segment combinés aux registres d’offset que l’on peut accéder les différentes zones de la mémoire conventionnelle. Un segment 8086 en mode réel ne peut pas être plus grand que 65536 octets. Cette limites à dérangé de nombreux programmeurs depuis très longtemps. Ces registres sont au nombre de 4 :

  • CS : « Code segment », il contient l’adresse du segment de la mémoire où se trouve les instructions (code) du programme. Remarquer qu’un programme peut être plus grand que 64k, il suffit de spécifier plusieurs segments de code. En changeant le segment CS, nous changeons de segment de code et nous pouvons en continuer l’exécution à partir de là.

  • DS : « Data segment », il contient l’adresse du segment de mémoire contenant les données définies dans le programme. De la même façon que CS, on peut utiliser plus que 64k de mémoire pour les données d’un programme.

  • SS : « Stack segment », il contient l’adresse de la pile (voir plus bas).

  • ES : « Extra segment », contient l’adresse d’un segment supplémentaire pouvant recevoir des données.

LA PILE (STACK)

En plus de registres, l'ordinateur dispose d'une pile. C'est une zone mémoire qui sert à sauver temporairement des informations. Elle possède une structure LIFO, c'est à dire last in first out, similaire à une pile de plateau à la cafétéria : le dernier plateau placé, donc celui sur le dessus de la pile, sera le premier pris. Sur la pile, nous empilons le contenu des registres généraux 16-bit. Nous verrons quels instructions utilise la pile dans la section suivante.

LES INSTRUCTIONS DE BASE DU LANGAGE ASSEMBLEUR

Avant de présenter quelques instructions de base du langage assembleur, voyons ensemble la syntaxe qui prévaut lorsqu’on écrit une source en assembleur. Premièrement, l’assembleur se fout complètement de la case des caractères, donc le majuscule et le minuscule est accepté. Règle générale, on s’entend pour écrire les commandes en majuscules. On doit séparé les commandes entre elles, d’au moins un espace. Les opérandes sont séparés par une virgule et dans ce cas un espace est facultatif :

MOV AX, 0
MOV AX, 0
MOV AX, 0
MOVAX,0 ; incorrect

On insère des commentaires dans le code avec le point virgule « ; ». Il existe naturellement des mots réservés inutilisables par le programmeur : les mnémoniques d’instructions, les directives d’assemblage, les registres et les mots (BYTE,WORD,DWORD).

LES VARIABLES EN ASSEMBLEUR

Les variables sont des zones mémoires qui sont réservées lors de l'assemblage. Ils fonctionnent de la même façon que les variables des langages de haut niveau. Voici comment déclarer des variables en assembleur :

-DB
Cette instruction définit et initialise des zones mémoires sur un octet, le début de la zone mémoire sera repéré par un identificateur, à notre choix. Équivalent à « char » en C.

Exemple:

chaine db « Ceci est une chaîne$ » ; quand une chaîne doit être affichée
nombre db 0 ; il faut rajouter '$' à la fin.
inconnu db (?) ; la variable n’a pas de valeur initiale

-DW

Cette instruction définit et initialise des zones mémoires sur un mot, c'est à dire 16 bit. C’est l’équivalent de « int » en C.

Exemple:

trois dw 1000,2000,3000
adresse dw 0FFFFh

-DD

Cette instruction définit et initialise des zones mémoires sur un double mot, c'est à dire 32-bit. Équivalent à « float » en C.

Exemple:
trois dw 1000,2000,3000
adresse dw 0FFFF000Ch

Les labels

Quand nous déclarons une variable, son nom pointe sur l’adresse mémoire où elle est stockée. De la même façon, nous pouvons placer des préfixes devant des instructions. Ce principe est souvent utilisé par les boucles.

Exemple:

debut:
MOV AX, 0 ; l'instruction MOV est associé au label début
MOV BH, 5 ;
JMP debut ; effectue une boucle au label "debut"

QUELQUES INSTRUCTIONS

Comme nous le savons, une instruction doit spécifier deux éléments importants :

  • L’opération à réaliser (addition, soustraction, décalage,…)
  • Les données manipulées par l’instructions. Nous verrons que ces données sont dans les registres ou en mémoire

Pour accéder à une donnée en mémoire, le microprocesseur devra connaître son adresse ET le nombre d’octets (bytes) qu’elle occupe.

MOV
Une des instructions les plus utilisées. Elle permet de manipuler différents types d'opérandes dont des zones de mémoires. Tout simplement, le contenu de l'opérande de droite est transféré dans l'opérande de gauche.

Exemples:

MOV AH, 12 ; AH <-- 12 (AL n'est pas affecté)
MOV BX, 1 ; BL & BH sont affectés

var1 DB ? ; définition d'une variable BYTE
var2 DW ? ; définition d'une variable WORD

MOV var1,12 ; opération sur 1 BYTE
MOV var2, 1 ; opération sur 1 WORD

Note : quand le type d'opérande est indéfini, il est possible que l'instruction ne permettre pas à l'assembleur de déterminer le type des opérandes. L'instruction suivante:

MOV data[BX], 1 sera donc complétée comme suit:
MOV BYTE PTR data[BX], 1 ; pour indiquer que l'opérande est de type BYTE

La commande MOV nous amène à un sujet important de la programmation assembleur : les différents modes d’adressages de la mémoire. En effet, il existe différentes possibilités de présentation d'une adresse. Nous allons les analyser en détails car elles peuvent être présentes dans la plupart des autres instructions de l’assembleur.

On spécifie une adresse mémoire sous la forme suivante :

[Segment : Offset]

Il serait évidemment très fastidieux d’avoir à spécifier un segment ET une offset pour chaque zone de mémoire. C’est pour cela que la plupart du temps, on assume que le registre DS est le segment de données par défaut. Ainsi, à la place de :

MOV AX, DS :[BX]
MOV AX, DS :[SI+2]

On pourra tout simplement écrire :

MOV AX, [BX]
MOV AX, [SI+2]

Nous verrons plus tard que les chaînes de caractères sont traitées de façon différentes. On peut naturellement outrepasser cette règle en spécifiant nous-même le segment :

MOV AX,CS :[BX]

ADDRESSAGES POSSIBLES

La commande MOV est utilisé dans cette série d’exemple car elle permets les 8 types distincts d'adressage sur des données ou sur des emplacements mémoire:

L'adressage immédiat

Registre <----- Valeur immédiate
Le contenu du registre est initialisé par une valeur immédiate, bref une constante.

EXEMPLE:

; transfert de 8 bits (1 octet)
MOV AH,12 ; AH <-- 12, AL n'est pas affecté
; transfert de 16 bits (2 octet ou 1 mot)
MOV BX,1 ; BX <-- 1, BL et BH sont affectés

L'adressage direct

Registre <------ mémoire (adressage direct)
Le contenu d'un registre est initialisé par le contenu de la zone mémoire repérée par l'identificateur. L'opération se fait sur 8 ou 16 bits.

EXEMPLE:

nb1 DB 5 ; zone d'un octet
nb2 DW 7 ; zone d'un mot

; AH <-- 5 opération sur 8 bits
MOV AH, nb1
; BX <-- 7 opération sur 16 bits
MOV BX, nb2
;Erreur (8 bits/16 bits)
MOV AL, nb2

L'adressage basé

Registre <------ [BX] adressage basé sur DS
Initialise le contenu d'un registre avec la zone mémoire située à l'adresse DS:[BX]. Dans ce cas, [BX] représente le contenu de la zone mémoire dont l'adresse (offset) est dans BX.

EXEMPLE:

; AX <--- contenu de la zone mémoire à l'adresse;
DS:0008
MOV BX,8
MOV AX,[BX]
; AX <--- contenu de la zone mémoire à l'adresse
; DS:nb1, donc 5
DW nb1 5
MOV BX, offset nb1 ; BX <-- offset de nb1
MOV AX,[BX]

L'adressage indexé

Registre <------ [SI] ou [DI] adressage indexé
Ce mode d'adressage est principalement utilisé pour les chaînes de caractères ou pour isoler un élément d'un tableau. ES:DI ou DS:SI

EXEMPLE:

;contenu de la zone mémoire à l'adresse DS:0
MOV SI,0
MOV AX,[SI]

L'adressage indexé et basé

Registre <------ [BX][DI] adressage indexé et basé DI
Registre <------ [BX][SI] adressage indexé et basé SI
Ce type d'adressage utilise BX et, SI ou DI, la base étant DS. On l'apelle basé car on l'utilise souvent pour référencer les éléments d'un tableau dont l'adresse de base est dans BX.

EXEMPLE:

; AH <-- contenu de la mémoire à l'Adresse DS:7 (5+2)
MOV BX,2
MOV SI,5
MOV AH,[BX][SI]

L'adressage basé et déplacement

Registre <------ donnee[BX]
Registre <------ [BX+donnee]

EXEMPLE:

Tableau DW 5 DUP (10,20,30,40,50)
; AX <--- troisième élément du tableau donc 30
MOV BX,2
MOV AX,Tableau[BX]

L'adressage indexé et déplacement

Registre <------ donnée[SI]
Registre <------ donnée[DI]

EXEMPLE:

; AX <-- contenu de la zone mémoire adresse DS:4+1
MOV SI,4
MOV AX,[SI+1]

L'adressage basé, indexé et déplacement
Registre <-- donnée[BX][SI]
Registre <-- donnée[BX][DI]

EXEMPLE:
; AX <-- contenu de la zone mémoire adresse DS:2+5+4+1
db1 DB 2
MOV BX,5
MOV SI,4
MOV AX,nb1[BX][SI+1]

Toutes ces différentes façon d’accéder aux données seront reprises dans les descriptions des autres instructions de l’assembleur. Reportez-vous à cette table pour vous rafraîchir la mémoire si vous en ressentez le besoin.

PUSH et POP
Les deux seules opérations sur la pile sont l'empilage (PUSH) et le désempilage (POP). La pile nous permet de sauvegarder temporairement les données. Lorsqu'une instruction PUSH est rencontrée, SP est d'abord décrémenté de 2 (chaque élément = 2 octets), et le contenu du registre spécifié est copié dans l'élément numéro SP de la pile. Pour une opération POP, l'élément numéro SP de la pile est copié dans le registre spécifié et SP est incrémenté de 2.

Exemple:
Pile de x éléments, vide:

100 SP --> SP pointe "avant" la pile
98 | | ; vide
96 | | ; vide

On exécute les instructions suivantes:
MOV AX,1 ; AX reçoit la valeur 1
MOV BX,2 ; BX reçoit la valeur 2
PUSH AX ; on empile AX
PUSH BX ; on empile BX

On aura le résultat suivant:
100 ; SP pointe sur 2
98 | 1 | ; 1er élément
96 SP-> | 2 | ; 2ième élément (LIFO)
94 | | ; 3ième élément vide

Maintenant, on exécute ceci:
MOV BX,3 ; BX reçoit 3
POP BX ; BX reçoit le dernier élément du stack (3 est écrasé)

Le stack aura l'air de cela:
100 ; SP pointe sur 2
98 SP-> | 1 | ; 1er élément
96 | 2 | ; 2ième élément (LIFO)
94 | | ; 3ième élément vide
NB: Lors du prochaine PUSH, 2 sera écrasé.

INT
Cette instruction appelle l’interruption dont le numéro est spécifié par l’opérande. Les interruptions sont utilisé par les périphériques pour signaler au processeur d’effectuer un arrêt afin qu’il puisse s’occuper d’eux. Il ne faut pas oublier de remplir de façon correcte les registres (généralement AX) avec les bons numéros d’interruptions. La liste complète des interruptions du BIOS se trouve facilement sur le net, en voici donc un bref aperçu :

Numéro Fonction
0D Disque dur
0E Disquette
0F Imprimante
10 Fonctions vidéos
11 Configuration système
12 Taille de la RAM
13 Fonctions disquette/disque dur
14 Accès à l’interface série
16 Interrogation du clavier

Programme exemple #1: "Hello World!"

Il est maintenant le temps pour vous d'écrire votre premier programme en assembleur. Nous verrons plus en détails l’utilisation de TASM au prochain chapitre. Pour l’instant, concentrez-vous sur les instructions que nous avons vue dans ce premier chapitre.

Ce fameux programme, utilisé par Lionel Ritchie, le créateur du langage C, s'adapte facilement en assembleur. Une chaîne de caractère est un ensemble d'octets, donc nous déclarons une variable DB, que nous initialisons à "Hello World!". Quand une chaîne de caractère est destinée à l'affichage, il faut ajouter impérativement le '$'. Ensuite, nous appelons la fonction 09h de l'interruption 21h. Cette fonction reçoit en paramètre le numéro 09h dans AH, ainsi que l'adresse de segment de la chaîne dans DS, et l'offset dans DX. Cette chaîne de caractères peut également contenir des codes ASCII (comme un retour de ligne).

.MODEL SMALL ; Modèle mémoire
STACK 100h ; Taille de la pile

.DATA ; Les donnees debute ici
chaine DB "Hello World!$" ; Initialise la variable "chaine"
; Ne pas oublier le $ à la fin

.CODE ; Le code débute ici.
START: ; Label du debut du programme
MOV AX, @DATA ; Initialise le segment des données DS
MOV DS, AX ; avec le contenu de DATA (chaine)

MOV DX,offset chaine ; DX pointe sur la chaîne MOV AH,09h ; Service 9h de l'interruption 21h
INT 21h ; On appelle l'interruption 21h (DOS)

MOV AX, 4C00H ; Fonction de retour
INT 21H ; au système d'exploitation
; On appelle l'interruption 21h (DOS)
END START ; Fin du programme
Vous n’avez qu’a recopier ce code dans votre éditeur de texte favori, et de le sauvegarder avec l’extension .asm. Ensuite, vous exécutez tasm.exe [nomfichier], et tlink.exe [nomfichier]. Le programme exécutable sera ainsi créé.

Je n’expliquerai pas ici toutes les subtilités du code. Au prochain chapitre nous verrons plus en détails les particularités de Turbo Assembler. Ce cours tire maintenant à sa conclusion. Il se peut que le lecteur se sente légèrement dépassé par toute la matière lors de la première lecture; il s’agit de ne pas se décourager et de lire tranquillement chaques sections. En résumé, nous avons vue au cours de ce chapitre :
  • Le fonctionnement interne d’un ordinateur
  • Les registres du processeur
  • Les segments de mémoire et la pile
  • Les variables DB, DW et DD ainsi que les labels
  • Les instructions assembleur MOV, INT, PUSH et POP
  • Les modes d’adressage en assembleur
  • Les interruptions du BIOS


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP2.rtf

CHAPITRE 2 - Utilisation de Turbo Assembler
L’assembleur TASM possède un éventail de fonctionnalités qui nous facilite grandement la vie lorsqu’on écrit des programmes. En effet, il propose, entres autres, un modèle simplifié de segmentation qui élimine du codage fastidieux. Nous étudierons en détails la structure d’un programme .COM et d’un .EXE, afin de bien comprendre les différences entres ces deux modèles.

INSTRUCTIONS DU MICROPROCESSEUR

Par défaut, l’assembleur assume que nous travaillons sur un microprocesseur 8086. Nous sommes donc limités dans le set d’instructions. Cependant, il est possible de spécifier le type de microprocesseur utilisé, par la simple command .[processeur] Par exemple :

.186 Permet l’assemblage d’instructions du 80186
.286 Permet l’assemblage d’instructions du 80286
.386 Permet l’assemblage d’instructions du 80386
.486 Permet l’assemblage d’instructions du 80486

MODÈLES MÉMOIRES

Si vous vous reportez au chapitre précédent, vous remarquerez que la première instruction du programme est la suivante :

.MODEL SMALL

Cette commande indique à l’assembleur que le modèle mémoire de ce programme est le modèle « small ». Le mode réel des PC se divise en six modèles mémoires basés sur la taille des données et du code. La raison est principalement pour l’adressage. En effet, le programme doit savoir s’il utilisera des pointeurs NEAR ou FAR. En résumé, un pointeur NEAR utilise seulement la portion « OFFSET » lorsqu’il adresse la mémoire; donc, il ne peut qu’adresser 1 seul segment de 64K. Les pointeurs FAR combine SEGMENT et OFFSET pour former des adresses de 20-bit, permettant d’adresser la totalité de la mémoire conventionnelle. Il va sans dire que les pointeurs FAR sont plus lent que les pointeurs NEAR. Voici la liste de ces modèles:

- Tiny : Code + Données < 64k (pour les fichiers .COM)
- Small : Code < 64k Données < 64k
- Medium : Code > 64k Données < 64k
- Compact : Code < 64k Données > 64k
- Large : Code > 64k Données > 64k
- Huge : Tableaux supérieurs à 64k

Le modèle Tiny est le seul qui permet la création de fichiers exécutable avec l’extension .COM. Dans ce cas, notre programme en entier peut être contenu dans un seul et unique segment de 64k. Sinon, le programme que nous assemblerons aura l'extension .EXE. Maintenant que nous connaissons les différents modèles mémoires et leurs raisons d’êtres, étudions la structure typique d’un programme .COM et .EXE.

Ébauche d'un programme .COM
Dans le cas d'un programme .COM, toutes les données sont contenus dans 1 seul segment, donc pas besoin de spécifié où CS et DS débutent puisqu'ils sont tous contenus dans le même. Également, les programmes .COM ne possèdent pas de pile, donc le SS est inutilisé. Remarquez aussi que les données se situe juste avant les instructions, et qu’on utilise l’instruction de saut JMP, pour débuter le programme au début des instructions. Voici le squelette d’un programme .COM typique sans utilisation de segmentation simplifiée :

.MODEL TINY
.486
ASSUME CS:CODE,DS:CODE ; Code dans même segment
ASSUME ES:NOTHING,SS:NOTHING ; ES et SS ne sont pas utilisé

CODE SEGMENT ; Segment de code
ORG 100H ; Adresse de début du programme

DEBUT: JMP START ; Début du programme principal

; <Données>

START: ; Début des instructions

PUSH CS ; Initialise
POP DS ; le segment de données

; <Instructions> ; Code ici

MOV AX,4C00H ; Termine le programme
INT 21H ; par une fonction de DOS
CODE ENDS ; Fin du segment
END DEBUT ; Référence le point d'entrée

Cependant, TASM nous offre un modèle simplifié pour déclarer nos segments. L’instruction .STARTUP gère le saut JMP que nous devions effectuer dans la version précédente. CODESEG et DATASEG initialise pour nous les segments, donc plus besoin de PUSH CS et POP DS et des ASSUME. Le tout est donc un code légèrement plus lisible :

.MODEL TINY ; modèle mémoire tiny
.486
CODESEG ; Initialise le segment de code
ORG 100h ; Point d’entrée du programme
.STARTUP ; Début des instructions

; <Instructions>

MOV AX,4C00H
INT 21H

DATASEG ; Initialise le segment de données (PUSH CS POP DS)
; <Données>

END ; Fin

Ébauche d'un programme .EXE
Dans le cas d'un programme .EXE, la macro DOSSEG nous permets d'utiliser la même segmentation que le DOS utilise, c'est à dire ASSUME CS:CODE, DS:DATA, ES:DATA. Il faut également définir une pile, avec la commande STACK suivit de la taille de la pile désirée. Pour ce faire, MOV AX, @DATA, suivit de MOV DS,AX. Voici un squelette d'un programme EXE:

DOSSEG ; Modèle de segmentation de DOS
.MODEL SMALL ; Petit modèle m‚moire
.STACK 100H ; 256 Octets pour la pile

.DATA
; <Données>

.CODE
START

MOV AX,@DATA ; Initialise le
MOV DS,AX ; segment de données

; <Instructions>

MOV AX,4C00H ; Fin du programme
INT 21H

END START

ASSEMBLAGE DES PROGRAMMES

Une fois notre programme terminé, nous sommes prêts à tester notre code. Pour ce faire, on tape la commande tasm <code>, où "code" est le nom du programme à assembler. Si tout va bien, le compilateur nous retourne quelques choses de semblable:

c:\asm>tasm prog1.asm
Assembling file: prog1.ASMError messages: NoneWarning messages: NonePasses: 1Remaining memory: 416k
c:\asm>

No
us obtenons alors le fichier de liens prog1.OBJ, qui est prêt à être linker. Le programme TLINK, fournit avec TASM, fait ce travail. Si nous voulons produire un .COM, il faut spécifier l'option /T, qui transforme le fichier OBJ en COM. Sinon, le linker retournera une erreur:

Po
ur un .COM
c:\asm>tlink /t prog1Turbo Link Version 7.0 Copyright (c) 1996 Borland International

Pour un .EXE
c:\asm>tlink prog1Turbo Link Version 7.0 Copyright (c) 1996 Borland International

NOTIO
NS GÉNÉRALES DE PROGRAMMATION

L'assembleur n'est pas un langage de programmation pour les débutants. Il est donc fort possible que vous ayez déjà programmer en C ou en PASCAL. Les mêmes méthodes de programmation peuvent s'appliquer en assembleur. Le langage assembleur supporte les même structures que les langages de haut niveau, tant pour les types de données complexes (RECORD, STRUCT) que pour les principes de bases (boucles, conditions, etc).

Bouc
l e FOR
Cette
boucle est une des plus utilisée. Nous utiliserons un label pour identifier le début de la boucle et l'instruction CMP, qui compare deux valeur. Si cette valeur est atteinte, la boucle peut se terminer, donc nous allons sauter avec JA, qui effectue le saut si supérieur. Voici un simple extrait d’un programme qui affiche le code ascii entre 41h et 7Ah à l’aide de la fonction du DOS 02h, écriture d’un car :

MOV
CL, 41h ; CL contient 41h ('A')for: CMP CL, 7Ah ; Plus grand que 7Ah JA endfor ; si oui, fini
MOV DL,CL ; mettre le car dans DL (pour affichage) MOV AH,02h ; fonction d'ecriture d'un car 02h INT 21h ; interruption DOS INC CL ; on incremente CL JMP for ; on recommenceendfor:

L'instruction
JMP fait retourner le fil d'exécution au label for, donc au début de la boucle. A chaque itération, la commande INC augmentera la valeur contenu dans CL de 1. On compare la nouvelle valeur ainsi de suite jusqu'à la fin de la boucle.

Boucle WHILE
D e la même faç
on, la boucle while s'adapte avec des label. La seule véritable différence est que la condition de sortie modifier le type de saut qu'on devra utiliser. Par exemple, on veut écrire un programme qui converti les caractères minuscules en majuscule, immédiatement après qu'une touche aie été enfoncée, et ce jusqu'à ce que l'utilisateur appuie sur ESCape. Notre condition de sorti sera si la touche ESC, code ASCII 27, est enfoncée. La commande JZ effectue un saut si la valeur du test est 0. Comme CMP effectue sa comparaison par soustraction, cela est idéal. Le seul hic, c'est que la fonction d'écriture du caractère modifie le contenu de AL. Nous allons donc devoir protéger AL, en utilise la Pile.

While: MOV AH
,08h ; fonction de lecture d'un car INT 21h ; interruption DOS CMP AL, 27 ; si ce caractère est pas ESC JZ Quit ; quitte le programme PUSH AX ; on sauvegarde AL MOV DL,AL ; mettre le car dans DL (pour affichage) MOV AH,02h ; fonction d'ecriture d'un car INT 21h MOV DL,124 ; mettre le car dans DL (pour affichage) MOV AH,02h ; fonction d'ecriture d'un car INT 21h POP DX ; on ramene AL de la pile dans DL SUB DL,32 ; soustraire 32 de son code ASCII MOV AH,02h ; fonction d'ecriture d'un car INT 21h MOV AH,9 ; pour faire les espaces MOV DX, OFFSET CRLF INT 21hJMP While
Quit:

Notre programme bouclera tant que
la touche ESC ne sera pas enfoncée. Avant d'imprimer le caractère lu, nous effectuons un PUSH du registre AX, qui sauvegarde notre registre AL, que nous ramenons dans DX avant d'en faire l'affichage en majuscule (Code ASCII - 32).

Boucle REPEAT
Question de mettre
un peu de sole
il dans votre journée, pourquoi ne pas remplir votre écran en mode texte de petit bonhomme sourire? La boucle repeat n’est pas plus complexe que les autres :

MOV BX, 2000

Repeat: MOV DL,02
MOV AH,02h INT 21h DEC BX CMP BX,0JG Repeat

Structure CASE - SWITCH
Ce prochain programme appelle une fonct
ion du BIOS pour avoir le jour courant. Le seul hic est que cette fonction retourne un nombre, soit 0 pour dimanche, 1 pour lundi, etc. Pas très utile, donc nous allons utilisez un switch pour déterminer quel chiffre correspond avec quel chaîne de caractères. Nous allons parcourir chaque choix, et aussitôt que nous trouvons notre jour, nous quittons le switch.

MOV AH,2Ah ; Fonction DOS 2Ah
(retourne des infos sur la date)INT 21h ; Interruption DOS (le jour est dans AL)
switch: ; plus clair avec label
Case1: CMP AL,0 ; CMPare AL avec 0 JNZ Case2 ; non? alors on continue a l'autre case... MOV DX, OFFSET Dimanche JMP EndswitchCase2:
CMP AL,1
JNZ Case3 MOV DX, OFFSET Lundi JMP Endswitch

Case3:
CMP AL,2 JNZ Case4 MOV DX, OFFSET Mardi JMP Endswitch
Case4:
CMP AL,3 JNZ Case5 MOV DX, OFFSET Mercredi
JMP Endswitch
Case5:
CMP AL,4 JNZ Case6 MOV DX, OFFSET Jeudi JMP Endswitch
Case6:
CMP AL,5 JNZ Case7 MOV DX, OFFSET Vendredi JMP Endswitch
Case7:
MOV DX, OFFSET Samedi Endswitch:

MOV AH,9 ; Fonction d'affichage d'une chaîne de carINT 21h
MOV AX,04ch INT 21h

Dimanche DB "Dimanche$"Lundi DB "Lundi$"Mardi DB "Mardi$"Mercredi DB "Mercredi$"Jeudi DB "Jeudi$"Vendredi DB "Vendredi$"Samedi DB "Samedi$"

Nous déclarons nos chaînes de caractères séparément. La condition JNZ
saute si le résultat n'est pas 0, donc si ce n'est pas le bon résultat. Le reste du code est répétitif et se comprend par lui même.

CODES SOURCES EXEMPLES

Ce chapitre contient de nombreux codes sources exemples. Le code est bien commenté, et si vous avez des questions n’oubliez pas de vous reporter à HelpPC, votre compagnon essentiel dans le monde de l’assembleur. Voici donc la liste des programmes exemples :

asmch2p1.asm > Programme qui demande le nom de l’usager version .COM
asmch2p2.asm > Même programme que asmch2p1.asm mais version .EXE
asmch2p3.asm > Boucle FOR
asmch2p4.asm > Boucle WHILE
asmch2p5.asm > Boucle REPEAT
asmch2p6.asm > Structure switch-case

Tous ces programmes compilent sous TASM, sans aucun problèmes. N’oubliez cependant pas l’option /T de TLINK pour les programme .COM. Nous avons vues plusieurs nouvelles instructions assembleur dans ce cours, notamment les instructions de saut. Elles sont très nombreuses et je ne les expliquerai pas en détails chacune. En résumé, nous avons vu :

Les modèles mémoires
La segmentation simplifiée de Turbo Assembler
Le
  • s boucles FOR, WHILE et REPEAT
  • La structure de décision switch-case
  • Les instructions CMP, INC, DEC, SUB
  • Les instructions de saut JZ, JNZ, JA


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP3.rtf

CHAPITRE 3 - Méthodes de programmation

Dans ce chapitre, nous effectuerons une bonne révision des concepts vues dans les deux premiers chapitres, afin de s’assurer d’avoir bien compris les principes fondamentaux de l’assembleur. Par la suite, nous entreprendrons d’écrire nos fonctions d’affichages et de traitements communs, pour créer notre première « librairie » assembleur de fonctions.

ORGANISATION D’UN PROGRAMME

Il existe plusieurs méthodes pour organiser un programme, la meilleur consiste à découper le programme en une série de sous-programmes (fonctions et procédures). Tous les exemples de ce chapitre seront présentés sous la forme de sous-procédures. Nous inclurons le fichier contenant nos procédures, de la même façon qu’en C nous utilisons les bibliothèques standard avec « #include <stdio.h> ». Rappelons que, par convention, les sous-programmes doivent sauvegarder les registres qu’ils utilisent (PUSH et POP avant d’effectuer un retour, ou USES). Comme nous le verrons au chapitre 5, certaines autres étapes viennent se rajouter quand on désire créer des fonctions assembleur qui seront utilisé par des compilateurs de haut niveau.

AFFICHAGES DE MESSAGES

Le système d’exploitation MS-DOS mets à notre disposition le servie 09h de l’interruption 21h pour afficher des chaînes de caractères, comme nous l’avons vu dans les chapitres précédent. Nous allons créer une procédure qui va grandement nous simplifier la vie :

; Affiche le message contenu dans DS:DX
AFFICHE PROC NEAR
PUSH AX ; Sauve AX sur la pile
MOV AH, 09h ; Fonction 09h
INT 21h ; Interruption DOS 21h
POP AX ; Ramène AX
RET ; Retour
AFFICHE ENDP

Nous remarquons tout d’abord qu’une procédure se déclare ainsi : <NOM> PROC [NEAR] [FAR], et se termine par <NOM> ENDP. Il est très important de ne pas oublier l’instruction RET qui rend le contrôle au programme principal. Si un programme possède beaucoup de messages, il serait utile de pouvoir transmettre simplement le numéro du message. Voici comment procéder :

Table DW mess0 ; mess0 = offset du message 0
DW mess1 ; mess1 = offset du message 1
DW mess2 ; mess2 = offset du message 2

mess0 DB ‘Bonjour je suis le message 1$’
mess1 DB ‘Bonjour je suis le message 2$’
mess2 DB ‘Bonjour je suis le message 3$’

;Affiche le message selon le numéro dans AL
AFFICHE PROC NEAR
PUSH BX ; on peut utiliser USES AX, BX
PUSH AX ; et laisse faire les PUSH et POP
MOV AH,0 ; Mets AH à 0 pour le décalage
SHL AX, 1 ; Multiplie le no du message par 2 pour
; obtenir un offset dans la table
MOV BX, OFFSET table ; BX pointe sur le début de table
ADD BX, AX ; avance BX sur l’offset du message
MOV DX, [BX] ; DX<- contenu de l’élément de table
; pointé par BX
MOV AH, 09h ; service 9 de l’interruption 21h
INT 21h ; affiche le message
POP AX
POP BX ; Ramène AX et BX
RET ; Ne pas oublier RET!
AFFICHE ENDP

Quand nous appelons cette procédure, il suffit de mettre dans le registre AL le numéro du message que l’on désire afficher sur l’écran. Si l’instruction SHL vous intimide, continuez la lecture de ce chapitre : une section entière est dédié pour ce sujet.

AFFICHAGE DÉCIMAL

Jusqu'à présent, vous remarquerez que nous avons seulement travaillé avec des chaînes de caractères. La raison en est bien simple : l’assembleur ne dispose d’aucune méthode pour afficher des nombres à l’écran, nous devons donc créer une procédure. Pour cela, il faudra décomposer le nombre et imprimer un par un les chiffres qui le composent. Nous aurons donc besoin de 2 procédures : affichage d’un chiffre et affichage d’un nombre.

La fonction 2 de l’interruption 21h est prévue pour afficher à l’écran un caractère contenu dans DL. Nous devons donc convertir le chiffre en son code ascii en ajoutant 48 :

Code Ascii Caractère
48 0
49 1
50 1
… …

Maintenant, nous devons décomposer le nombre à afficher. Nous savons que la divison (DIV) divise un WORD (numérateur dans AX) par une opérande sur 8 bits (dénominateur) et installe le quotient dans AL et le reste dans AH, ou divise un double mot dans DX AX et installe le quotient dans AX et le reste dans DX. Par exemple :

8765 DIV 10 = 876 reste 5
876 DIV 10 = 87 reste 6
87 DIV 10 = 8 reste 7
8 DIV 10 = 0 reste 8

Nous constatons que les restes sont les chiffres composant le nombre. Nous ne pouvons pas afficher le reste à chaque division car sinon le nombre serait à l’envers, il faut donc sauvegarder temporairement ces valeurs sur la pile, ce qui nous arrange bien car le dernier empilé sera le premier affiché et le premier empilé le dernier, et de cette façon le nombre sera correct.

Attention, bien que le nombre à afficher soit sur 16 bits, nous devons « simuler » une division sur 32 bits. En effet, dans l’exemple précédant 876 ne rentre pas dans AL (max 255), nous allons donc diviser le nombre dans DX,AX par 10, le quotient sera placé dans AC mais DX sera chaque fois remis à 0 avant la division puisque le numérateur est toujours sur 16 bit (dans AX).

; Affiche un nombre en décimal non signé dans AX
AffDecimal PROC NEAR
PUSH BX ; Sauvegarde BX
PUSH CX ; Sauvegarde AX
MOV BX,10 ; BX sera notre dénominateur
MOV CX, 0 ; Le compteur de chiffre
Decompose :
XOR AX,AX ; AX est remis a 0
DIV BX ; Calcule DX,AX/BX le reste dans DX
PUSH DX ; Sauve le chiffre sur la pile
INC CX ; Incrémente le compteur de chiffre
CMP AX, 0 ; Quotient=0?
JNZ Decompose ; Jump NonZero (saut si pas 0)

; Affichage des chiffres
Affichage :
POP AX ; Récupère un chiffre sur la pile
CALL AffChiffre ; Procédure qui affiche un chiffre
LOOP Affichage ; CX = CX - 1 (Loop tant que CX >0)

POP CX
POP BX
RET
AffDecimal ENDP

Le principe est le même pour les autres bases, tel hexadécimal, binaire et octal. Il suffit de changer le dénominateur par le nombre représentant la base désirée. Cette procédure ne traite cependant pas les nombres signés et les nombres à virgule flottante.

POSITIONNEMENT DU CURSEUR

La position du curseur est importante car elle influence les procédures d’affichage et de lecture du clavier. Cette procédure appelle le service 02h de l’interruption 10h (vidéo) du BIOS.

; Positionne le curseur à la colonne DL et à la ligne DH
GotoXY PROC NEAR
PUSH BX
PUSH DX

MOV AH,2
MOV BH,0
INT 10h

POP DX
POP BX
RET
GotoXY ENDP

LECTURE D’UNE CHAÎNE DE CARACTÈRES

Pour lire une chaîne de caractères, nous pouvons appeler l’interruption 21h, service 0Ah qui installe les caractères tapés dans une zone repérée par DS :DX (buffer). Il faudra donc définir une zone de données (buffer) dans le programme qui recevra les caractères.La fonction se termine quand l’usager appuie sur ENTER. Le buffer contient alors les informations suivantes :

Octets Contenu
0 Valeur indiquant le nombre maximum de caractères à lire, cette valeur doit être installée AVANT l’appel de la fonction.
1 Nombre de caractères lus(sans compter le RETURN), cette valeur est installée par la fonction
2 A partir de cette position, on trouve les caractères lus.

tampon DB 255 DUP (?) ; Zone mémoire tampon contenant jusqu'à 255 car

; Lit AL caractères dans DS :DX
LireChaine PROC NEAR
PUSH BX
PUSH CX
MOV BX,DX ; BX <- offset du buffer
PUSH [BX] ; préserve les deux premiers octets du buffer
MOV [BX], AL ; installe le nombre de caractères à lire en position 0 du buffer
MOV AH,0Ah ; fonction 0Ah, lire une chaîne de car
INT 21h
POP CX
POP BX
RET
LireChaine ENDP

Il faudra bien entendu assigner DX sur l’offset du tampon qui recevra les caractères avant d’appeler cette fonction. Si l’usager essaie d’entrer plus de caractère qu’il est permis (contenu dans AL), l’interruption 21h émet un beep.

FONCTIONS ARITHMÉTIQUES ET LOGIQUES

Nous allons maintenant aborder les instructions arithmétiques ainsi que les principes d’addition binaire, de soustraction, de négation, etc. Les instructions de base sont naturellement : l’addition, la soustraction, la multiplication et la division.

L’addition ADD
Cette instruction additionne deux quantités numériques sur 8 ou 16 bits, le résultat pouvant être placé en mémoire ou dans un registre.

L’incrémentation INC
Ajoute 1 au registre spécifié, le résultat obtenu est donc identique à celui d’un ADD avec 1 comme seconde opérande.

La soustraction SUB
Soustrait une quantité d’une autre, le résultat pourra être placé en mémoire ou dans un registre.

La décrementation DEC
Décremente de 1 la valeur du registre spécifié, le résultat est donc identique à celui d’un SUB avec 1 comme seconde opérande.

La multiplication MUL
Multiplie l’opérande spécifiée par la quantité contenu dans le registre AX ou AL (accumulateur). Le résultat est placé dans AH et AL si les opérandes étaient des octets et dans DX et AX si les opérandes étaient des mots.

La division DIV
Divise un mot (dénominateur dans AX) par une opérande sur 8 bits (dénominateur) ou divise un double mot (dans DX,AX) par une opérande sur 16 bits. Le quotient est placé dans AL et le reste dans AH si l’opérande était un octet, et le quotient dans AX et le reste dans DX si c’était un mot.

L’instruction NEG
Cette instruction calcule le complément à 2 de l’opérande spécifié. Rappelons que pour obtenir le complément à 2 d’une valeur, il faut d’abord inverser tous les bits et ajouter 1. La technique du complément à 2 obtient la représentation binaire d’un nombre.

L’instruction AND
L’instruction AND effectue un ET logique sur les deux opérandes spécifiées, le résultat est placé dans la première opérande. Rappelons qu’un ET logique donne le résultat 1 si les deux opérandes sont 1 et donne 0 dans les autres cas. Les combinaisons possibles sont donc :

0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1

Cette instruction est souvent utilisés pour masquer des bits.

L’instruction OR
Effectue un OU logique (Inclusif) sur les opérandes spécifiés, le résultat est placé dans la première opérande. Rappelons qu’un OU logique donne un résultat 0 si les deux opérandes sont 0 et donne 1 dans les autres cas. Les combinaisons possibles sont donc :

0 OR 0 = 0
1 OR 0 = 1
1 OR 1 = 1
0 OR 1 = 1

L’instruction XOR
Effectue un OU logique (Exclusif) sur les opérandes spécifiés, le résultat est placé dans la première opérande. Rappelons qu’un OU Exclusif donne un résultat 1 si les deux opérandes sont différentes et donne 1 dans les autres cas. Les combinaisons possibles sont donc :

0 XOR 0 = 0
1 XOR 0 = 1
1 XOR 1 = 0
0 XOR 1 = 1

L’instruction NOT
Effectue un NON logique sur l’opérande spécifiée; le résultat est placé dans l’opérande. Rappelons qu’un NON logique inverse (complémente) la valeur d’un bit. Les deux combinaisons possible sont donc :

NOT 0 = 1
NOT 1 = 0

L’instruction SHL (décalage logique à gauche)
Cette instruction déplace les bits d’un registre ou d’un emplacement mémoire d’un nombre de positions vers la gauche. Les positions libérées sont remplies avec des 0. Il s’agit d’un moyen rapide d’effectuer des multiplications par des puissances de 2.

L’instruction SHR (décalage logique à droite)
Cette instruction déplace les bits d’un registre ou d’un emplacement mémoire d’un nombre de positions vers la droite. Les positions libérées sont remplies avec des 0. Il s’agit d’un moyen rapide d’effectuer des divisons par des puissances de 2.

RÉCAPITULATION

En conclusion, voici une liste aide mémoire pour vérifier que vous avez bien compris les concepts énoncés dans les trois premiers chapitres. Si vous éprouvez des difficultés, il serait sage de relire certains passages concernant les notions en questions. En voici la liste :

  • [ ] permet de spécifier une zone mémoire plutôt qu’un registre directement
  • Une instruction ne peut jamais contenir 2 opérandes mémoires :

MOV [BX], [SI] ; incorrect
MOV Id1, [BX] ; incorrect

  • Certaines instructions possèdent plusieurs variantes, il n’est pas possible d’en utiliser d’autres. Si on souhaite exécuter une opération non prévue, il faudra contourner le problèmes avec des instructions prévues :

MOV ES, DS ; incorrectMOV AX, DS MOV ES, AX ; correct
S
  • i le type des opérandes ne permets pas à l’assembleur de déterminer s’il s’agit d’une opération sur un BYTE ou WORD, il faudra compléter avec BYTE PTR ou WORD PTR.
  • Lorsqu’on travaille sur des chaînes de caractères, l’instruction REP décrémente automatiquement le registre CX (voir exemple plus bas)
  • L’instruction LOOP décrémente le registre CX automatiquement


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP4.rtf

CHAPITRE 4 - Accès aux périphériques
Si l’assembleur n’est plus utilisé comme outil de développement dans l’industrie, il reste que sa fonctionnalité primaire, celle d’interagir avec la machine, est toujours exploitée. En effet plusieurs langages de haut niveau n’ont pas dans leurs librairies standard la capacité d’interroger les ports d’entrées/sorties, l’imprimante, les disques durs et autres périphériques. L’assembleur reste donc une solution très puissante dans ces cas. Dans ce chapitre, nous verrons quelques exemples de programmation assembleur des périphériques. Comme nous serons souvent appelé à tester l’état de ces périphériques, nous verrons également plus en détails les différentes opérations de saut conditionnels possibles.

FLAGS

Les flags, "indicateurs", sont des bits qui regroupés dans le registre de flag. Ces flags sont modifiés en fonction de l'exécution des différentes instructions. Ils sont utilisés en relation avec les opérateurs de comparaisons, que nous allons étudier par la suite.

CF
Le Carry Flag est celui qui contient la retenue d’une opération, lorsque le résultat dépasse la capacité du registre. Le bit est alors placer dans le CF.

PF
Il s'agit du Parity Flag. La valeur de ce flag est 1 si le nombre de bits d'une opérande est pair.

AF
AF est l'auxiliary carry qui s’apparente au CF.

ZF
Zero Flag mis à 1 lorsque un résultat est égal à 0. Souvent utilisé pour les diverses opérations, il est utile pour éviter des problèmes de divisions sur 0.

SF
Sign Flag passe à 1 si nous avons un résultat signé (négatif)

IF
IF pour Interrupt Flag, enlève la possibilité au processeur de contrôler les interruptions.
Si IF=0, le processeur ne commande pas et si IF=1 alors c'est le contraire.

DF
Direction Flag. C'est ce Flag qui donne l'indication sur la manière de déplacer les pointeurs lors des instructions de chaîne (MOVSB,STOSB) soit positivement, soit négativement.

OF
Overflow Flag (indicateur de dépassement). Il permet de trouver et de corriger
certaines erreurs produites par des instructions mathématiques.

SAUTS CONDITIONNELS

Dans la programmation de périphériques, comme dans tous les programmes, il faut souvent avoir recours aux sauts pour déterminer le chemin à suivre dans notre programme. Par exemple, si on veux savoir si l’imprimante est connectée, il faut interroger le port parallèle et agir soit en initialisant l’imprimante, ou en avisant l’usager avec un message d’erreur.

JB - JNAE - JC
Below - Not Above or Equal - Carry
CF = 1

JAE - JNB - JNC
Above or Equal - Not Below - Not Carry
CF=0

JE - JZ
Equal - Zero
ZF=1

JNE - JNZ
Not Equal - Not Zero
ZF=0

JO - JNO
Overflow - Not Overflow
OF=1 - OF=0

JP - JPE
Parity - Parity Even
PF=1

JNP - JPO
No Parity - Parity Odd
PF=0

JS - JNS
Signed - Not Signed
SF=1 - SF=0

JA - JNBE
Above - Not Below or Equal
CF=0 et ZF=0

JBE - JNA
Below or Equal - Not Above
CF=1 ou ZF=1

JG - JNLE
Greater - Not Less or Equal
ZF=0 et SF=OF

JGE - JNL
Greater or Equal - Not Less
SF=OF

JL - JNGE
Less - Not Greater or Equal
SF (signé)=OF

JLE - JNG
Less or Equal - Not Greater
ZF=1 ou SF (signé)=OF

L'instruction CMP

Pour tester tout cela, nous avons besoin d'une instruction, c'est CMP qui nous aidera à le faire. Cette instruction va nous servir à tester différentes valeurs et modifier les flags en fonction du résultat. CMP est un SUB qui ne change ni la source ni la destination, seulement les flags. Un CMP BX,CX sera comme un SUB BX,CX à l’exception près que BX ne sera pas modifié. Si BX=CX alors BX-CX=0 donc le flag ZF sera égal à 1. Si nous voulons faire un saut avec "égal à" (JNE ou JZ qui demande ZF=1), nous avons ZF=1 et comme JZ demande que ZF=1 pour faire le saut, nous avons donc le saut.

Exemples pratiques

Les exemples de ce chapitre démontre l’application pratique de ces opérateurs de saut conditionnels. Dans le premier exemple, on vérifie si une souris est présente, et on quitte le programme si ce n’est pas le cas. Remarquez dans le deuxième programme, qui démontre la programmation de l’imprimante, j’utilise l’opérateur TEST, qui effectue lui un ET logique sur les opérandes, mais qui va modifier les Flags.

Dans ce même exemple, je présente aussi brièvement le traitement des chaînes des chaînes de caractères. En effet, il existe une série d’opérateur spécifiques pour les chaînes de caractères et leurs traitements. Voici un résumé de ces fonctions :

LODSB Charge dans AL le contenu de DS :SI. SI et DI sont incrémentés ou décrémentés selon la valeur de DF.
STOSB Mets dans ES :DI le contenu de AL. SI et DI sont incrémentés ou décrémentés selon la valeur de DF.

MOVSB Mets dans ES :DI le contenu de DS :SI. SI et DI sont incrémentés ou décrémentés selon la valeur de DF.

Toutes ces instructions viennent aussi en format Word (16-bit) et Dword (32-bit). On remplace les deux dernières lettres, donc LODSW, STOSW, MOVSW, LODSD, STOSD et MOVSD. Ces instructions sont souvent utilisés en programmation graphique pour effectuer des transferts rapides entre les écrans virtuels, les sprites et l’écran physique.

Résumé :

  • Les indicateurs d’états (FLAG)
  • L’instruction CMP et TEST
  • Les instructions de chaînes LOD, STO et MOV (version SB,SW,SD)


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP5.rtf

CHAPITRE 5 - Assembleur & C

L'inclusion de modules en assembleur dans les programmes de haut niveau constitue un objectif fondamental de la programmation en assembleur. Si les langages de haut niveau présentent beaucoup d'avantages et sont plus agréables à manier que l'assembleur, il leur arrive de ne pas connaître certaines fonctions indispensable à un projet. Également, pour le traitement des boucles interne (inner loops), il est carrément impératif d'utiliser l'assembleur. Il existe 3 façon d’inclure de l’assembleur dans nos sources C. On peut avoir recours à l’assembleur interne du compilateur, utiliser un assembleur externe directement dans notre source ou à l’extérieur de celui-ci.

Utilisation du compilateur interne

Tous les compilateurs modernes ont un assembleur interne qui permet d’inclure directement dans du code source des lignes de codes assembleur. En Borland C++, il s’agit de l’instruction asm. Nous avons déjà vu sont utilisation, comme par exemple :

void mode_graphique()
{
asm {
MOV AX, 0x13
INT 0x10
}
}

L’assembleur interne est cependant beaucoup moins efficace qu’un assembleur externe tel MASM ou TASM. Je n’expliquerai pas ici en détails tous les routines traduites en assembleur, car à présent vous devriez être capable de suivre le déroulement d’une fonction assembleur à partir du code source. J’ai néanmoins commenté le code pour vous aider dans cette tâche.

Inclure des routines de bas niveau dans le C

Ce processus s'effectue en 2 étapes: premièrement, il faut coder les fonctions elles-mêmes dans un fichier source ASM. Ensuite, il faut que le compilateur C puisse y référer. Cependant, il faudra légèrement modifier notre code asm pour que nos fonctions soient à même d'être adressées et encore plus important, qu'elles puissent recevoir des paramètres.

Tous les langages de haut niveau ont une exigence commune vis-à-vis du module en assembleur: ils réclament l'installation d'un cadre de pile. Il s'agit en fait d'un dédoublement du segment de pile dans le registre BP. Comme la pile du langage de haut niveau de doit pas être modifier, le contenu du pointeur de pile est copié en BP. Avant cela, l'ancienne valeur de BP est sauvegardée sur la pile. Donc, tous modules assembleurs destinés à être incorporé dans du code de haut niveau doit commencer par les instructions:
PUSH BP
MOV BP,SP

Également, chaques fonctions doit être déclarée PUBLIC, c'est à dire la rendre accessible à tous les modules du projet. Pour restaurer la pile à son état original, il ne faut pas oublier l'évidence:

POP BP

Le nom de notre fonction dans le module assembleur devra être précédé du caractère de soulignement, _ ("underscore"). La commande ARG nous permet de spécifier les paramètres que notre fonction recevra. Par exemple, notre fonction pour choisir un mode vidéo par le BIOS deviendrait, si on veut l'utiliser avec un module externe, la suivante:

PUBLIC _setmode ; Utilisable par tous les modules
_setmode PROC ; PROCédure setmode (avec le _)
 ARG mode:WORD ; Paramètre "mode"
 PUSH BP ; sauvegarde bp
 MOV BP,SP ; sur la pile
MOV AX, mode ; ax = mode
INT 10h ; interruption vidéo
POP BP ; on ramène la pile originale
 RET ; toujours mettre RET
_setmode ENDP ; fin de la procédure

Modifications dans notre code C

Pour compiler correctement notre code dans l'environnement C, il faudra créer un fichier project. Pour ce faire, dans le menu Project, il faut ouvrir un nouveau projet, avec Open Project. Donnez un nom à votre projet, par exemple, test. Un nouveau fichier PRJ sera créé. Cette fois, dans le menu PRJ, aller sur Add Item. Ajouter votre fichier .ASM, ainsi que votre fichier CPP. Donc:

PROJECT --> OPEN PROJECT
PROJECT --> ADD ITEM -> "fichier.ASM"
PROJECT --> ADD ITEM -> "fichier.CPP"

La fenêtre "Project" devrait apparaître en bas de l'écran avec la liste des fichiers que vous avez inclus. Pour rendre accessible les modules Assembleur, il faut déclarer nos fonctions avec le mot-clef "extern". Cela signifie que la définition de la fonction se trouve dans un module exterieur au C. Par
exemple:

extern "C"
{
void setmode(unsigned int mode);.

etc...
}

Maintenant, vous êtes prêt à compiler. Assurez-vous que TASM.EXE et TASM2MSG sont tous les deux présents dans votre PATH (pour être sûr, copier ces 2 fichiers dans c:\windows\command). Si tout ce passe bien, votre code asm sera compilé par TASM, et le CPP par le compilateur C. Si l'environnement Borland vous envoie un message style "Cannot find executable : TASM", il faut que vous spécifier le chemin de tasm.exe:

OPTIONS --> TRANSFER...
EDIT --> ~Turbo Assembler
PATH --> (le chemin), ex: C:\TC\BIN\

Invoquer un assembleur externe sans utiliser le fichier projet

Il est possible d’utiliser de l’assembleur beaucoup plus simplement, en l’incluant directement dans le code source. Il existe en effet une instruction du pré-processeur qui signale que l’assembleur interne doit faire place à un assembleur externe pour le traitement des lignes de codes assembleur. Il s’agit de #pragma inline. Par exemple :

// Exemple d'appel d'inline assembly par TASM

#pragma inline
asm .386p
asm MESSAGE DB "Allo de Turbo Assembleur dans C++$",13,10

void main()
{
asm MOV AX, 3
asm INT 0x10
asm MOV AH, 9
asm XOR AL,AL
asm MOV DX, OFFSET MESSAGE
asm INT 0x21
}

Programmes exemples

Les programmes exemples mettent en pratique les différentes façon d’appeler de l’assembleur dans des programmes C. Règle générale, l’assembleur interne est plus facile d’utilisation mais est moins efficace que l’assembleur externe.


ASM Mode Reel

.MODEL COMPACT                       ; modele memoire compact
.486                                 ; instructions 486
.CODE                                ; segment de code
PUBLIC _setmode,_clear,_hline,_copy  ; declaration des fonctions externes

; ----------------------------
; *	SETMODE              *
; ----------------------------

_setmode	PROC                 ; Specifie le mode video passer
                     ; en parametre
    ARG	mode:WORD            ; parametre mode
    PUSH	BP                   ; pour appel de fonctions
    MOV	BP,SP                ; par programme externe de haut niveau

    MOV	AX, mode             ; numero du mode dans AX
    INT	10h                  ; interruption video

    POP	BP                   ; sortir de la fonction
    RET                          ; fin de la fonction

_setmode	ENDP

; ----------------------------
; *          CLEAR           *
; ----------------------------

_clear		PROC
    ARG	ecran:DWORD
    PUSH	BP                  ; pour appel de fonctions
    MOV	BP,SP               ; par programme externe de haut niveau
    PUSH DI

    LES  DI,ecran               ; ES:DI pointe sur l'ecran
    MOV  CX,16000               ; 16000 iterations (64000/4)
    XOR  AX,AX                  ; AX a 0
    REP  STOSD                  ; repete deplace 4 octet de valeur AX (0)
                    ; vers ES:DI
    POP  DI
    POP	BP                  ; sortir de la fonction
    RET                         ; fin de la fonction

_clear		ENDP

; ----------------------------
; *          CLEAR           *
; ----------------------------
_copy	PROC
    ARG	ecran:DWORD
    PUSH	BP                 ; pour appel de fonctions
    MOV	BP,SP              ; par programme externe de haut niveau
    push es
    push	ds
    push	di
    push	si

    MOV  AX, 0A000h            ; destination ecran
    MOV  ES, AX
    XOR  DI, DI                ; coin superieur gauche
    LDS  SI, ecran             ; DS:SI = ecran virtuel
    MOV  CX, 16000             ; 16000 iterations (64000/4)
    REP  MOVSD                 ; repete deplace 4 octet

    pop	si
    pop	di
    pop	ds
    pop  es
    POP	BP                 ; sortir de la fonction
    RET                        ; fin de la fonction
_copy     ENDP

; ----------------------------
; *	    HLINE            *
; ----------------------------

_hline  PROC
    ARG  y:WORD, col:BYTE, ecran:DWORD

    PUSH	BP                ; pour appel de fonctions
    MOV	BP,SP             ; par programme externe de haut niveau
    PUSH DI

    LES  DI,ecran
    MOV  AX,y
    MOV  DI,AX
    SHL  DI,8
    SHL  AX,6
    ADD  DI,AX
    ADD  DI,4
    MOV  AL,col
    MOV  AH,AL
    MOV  CX,160
    REP  STOSW

     POP  DI
    POP	BP               ; sortir de la fonction
    RET                      ; fin de la fonction

_hline  ENDP

END


ASM Mode Reel

#include <time.h>
#include <conio.h>
#include <stdio.h>

#define vga 0xA000
#define MIN_Y 20
#define MAX_Y 160

char *virtuel = new char[64000L];

extern "C"
{
    void setmode(int mode);
    void clear(char *);
    void copy(char *);
    void hline(int,unsigned char,char *);
}

//----------------------------------------------------------------------//
// setpal - fixe les attributs r,g,b                                    //
//----------------------------------------------------------------------//
void setpal(unsigned char col,unsigned char r, unsigned char g, unsigned char b)
{
  outp (0x03C8,col); // Envoie les informations au DAC
  outp (0x03C9,r);
  outp (0x03C9,g);
  outp (0x03C9,b);
}

//----------------------------------------------------------------------//
// preparepal - modifie la palette a mon gout                           //
//----------------------------------------------------------------------//
void preparepal()
{
  for (int i=1;i<=10;i++)  setpal(i,0,0,i*3);
  for (i=11;i<=20;i++)	  setpal(i,(i-10)*3,0,0);
  for (i=21;i<=30;i++)	  setpal(i,0,(i-20)*3,0);
  for (i=31;i<=40;i++)     setpal(i,0,(i-30)*3,(i-30)*3);
  for (i=51;i<=60;i++)     setpal(i,(i-50)*3,(i-50)*3,(i-50)*3);
  for (i=61;i<=70;i++)     setpal(i,(i-60)*3,(i-60)*3,0);
  for (i=71;i<=80;i++)     setpal(i,(i-70)*2,(i-70)*3,(i-70));
}

//----------------------------------------------------------------------//
// Barre - Anime trois barres sur l'ecran                               //
//----------------------------------------------------------------------//
void barre(int &y, unsigned char coul, int &sens)
{
  int ligne,i,x;

  for(ligne=0;ligne<=10;ligne++)
  {
    hline(y+ligne,coul,virtuel);
    if(ligne < 5) coul++;
    if(ligne > 5) coul--;
    if(y>=MAX_Y)  sens=-1;
    if(y<=MIN_Y)  sens=1;
  }
  y+=sens;
}

void main()
{
  int sensg =  1;
  int sensb =  1;
  int sensr = -1;
  int sensv =  1;
  int sensa =  1;
  int sensc =  1;
  int sensd =  1;

  int yb = MIN_Y;
  int yr = MIN_Y + 20;
  int yg = MIN_Y + 20;
  int yv = MIN_Y + 40;
  int ya = MIN_Y + 60;
  int yc = MIN_Y + 80;
  int yd = MIN_Y + 100;
  unsigned long tempdeb, tempfin, frame;

  setmode(0x13);
  preparepal();

  frame=0;tempdeb=0;tempfin=0;
  tempdeb = clock();
  do
  {
    frame++;
    clear(virtuel);
    barre(yr,15,sensr);
    barre(yb,5, sensb);
    barre(yg,25,sensg);
    barre(yv,35,sensv);
    barre(ya,55,sensa);
    barre(yc,65,sensc);
    barre(yd,75,sensd);
    copy(virtuel);
  } while (frame < 5000);
  tempfin = clock();

  setmode(0x03);
  printf("Temps pour dessiner 5000 frames: %u\n", tempfin - tempdeb);
  printf("\nShaun Dore\ndores@videotron.ca\nhttp://pages.infinit.net/shaun/");
}


ASM Mode Reel

//----------------------------------------------------------------------//
// FICHIER              : 2DCHAP4.CPP       asmch5p3.ccp                //
// AUTEUR               : Shaun Dore                                    //
// DESCRIPTION          : Animations et ecrans virtuels                 //
// DATE DE MODIFICATION : 02-02-98	                                //
// COMPILATEUR          : Borland Turbo C++ Real Mode 16-bit compiler   //
// NOTES                : Compiler avec modele memoire Compact          //
//----------------------------------------------------------------------//

//----------------------------------------------------------------------//
// Fichiers include                                                     //
//----------------------------------------------------------------------//

#include <mem.h>
#include <time.h>
#include <stdio.h>
#include <conio.h>

//----------------------------------------------------------------------//
// Constantes                                                           //
//----------------------------------------------------------------------//

#define MIN_Y 10
#define MAX_Y 170

//----------------------------------------------------------------------//
// Variables globales                                                   //
//----------------------------------------------------------------------//

char *ecran  = (char *) (0xA0000000L);       // ptr sur RAM Video
char *virtuel = new unsigned char[64000L];   // ptr sur ecran virtuel

//----------------------------------------------------------------------//
// setmode - Appelle le mode passer en parametre                        //
//----------------------------------------------------------------------//
void setmode(unsigned int mode)
{
  asm {
       MOV AX, [mode]
       INT 0x10
     }
}

//----------------------------------------------------------------------//
// clsvirt - vide l'ecran                                               //
//----------------------------------------------------------------------//
void clsvirt()
{
  memset(virtuel,0,64000L);
}

//----------------------------------------------------------------------//
// cpyvirt - copie un ecran vers un autre                               //
//----------------------------------------------------------------------//
void cpyvirt()
{
  memcpy(ecran,virtuel,64000L);
}

//----------------------------------------------------------------------//
// putpixel - Affiche un pixel                                          //
//----------------------------------------------------------------------//
void putpixel(int x, int y, unsigned char col)
{
  virtuel[(y*320)+x] = col;
}

//----------------------------------------------------------------------//
// hline - Dessine une ligne horizontale                                //
//----------------------------------------------------------------------//
void hline(int y, unsigned char coul)
{
  memset(virtuel+(y*320),coul,320);
}

//----------------------------------------------------------------------//
// setpal - fixe les attributs r,g,b                                    //
//----------------------------------------------------------------------//
void setpal(unsigned char col,unsigned char r, unsigned char g, unsigned char b)
{
  outp (0x03C8,col); // Envoie les informations au DAC
  outp (0x03C9,r);
  outp (0x03C9,g);
  outp (0x03C9,b);
}

//----------------------------------------------------------------------//
// preparepal - modifie la palette a mon gout                           //
//----------------------------------------------------------------------//
void preparepal()
{
  for (int i=1;i<=10;i++)  setpal(i,0,0,i*3);
  for (i=11;i<=20;i++)	  setpal(i,(i-10)*3,0,0);
  for (i=21;i<=30;i++)	  setpal(i,0,(i-20)*3,0);
  for (i=31;i<=40;i++)     setpal(i,0,(i-30)*3,(i-30)*3);
  for (i=41;i<=50;i++)     setpal(i,(i-40)*3,(i-40)*3,(i-40)*3);
  for (i=61;i<=70;i++)     setpal(i,(i-60)*3,(i-60)*3,0);
  for (i=71;i<=80;i++)     setpal(i,(i-70)*2,(i-70)*3,(i-70));
}

//----------------------------------------------------------------------//
// Barre - Anime trois barres sur l'ecran                               //
//----------------------------------------------------------------------//
void barre(int &y, unsigned char coul, int &sens)
{
  int ligne,i,x;

  for(ligne=0;ligne<=10;ligne++)
  {
    hline(y+ligne,coul);
    if(ligne < 5) coul++;
    if(ligne > 5) coul--;

    if(y>=MAX_Y)  sens=-1;
    if(y<=MIN_Y)  sens=1;
  }
  y+=sens;
}

//----------------------------------------------------------------------//
// Fonction MAIN                                                        //
//----------------------------------------------------------------------//

void main()
{
  int sensg =  1;
  int sensb =  1;
  int sensr = -1;
  int sensv =  1;
  int sensa =  1;
  int sensc =  1;
  int sensd =  1;

  int yb = MIN_Y;
  int yr = MIN_Y + 20;
  int yg = MIN_Y + 20;
  int yv = MIN_Y + 40;
  int ya = MIN_Y + 60;
  int yc = MIN_Y + 80;
  int yd = MIN_Y + 100;
  unsigned long tempdeb, tempfin, frame;

  setmode(0x13);
  preparepal();

  frame=0;tempdeb=0;tempfin=0;
  tempdeb = clock();
  do
  {
    frame++;
    clsvirt();
    barre(yr,15,sensr);
    barre(yb,5, sensb);
    barre(yg,25,sensg);
    barre(yv,35,sensv);
    barre(ya,45,sensa);
    barre(yc,65,sensc);
    barre(yd,75,sensd);
    cpyvirt();
  } while (frame < 5000);
  tempfin = clock();         // clock() pour le temps de fin
  setmode(0x03);

  printf("Temps pour 5000 frames %u\n", tempfin - tempdeb);
  printf("\nShaun Dore\ndores@videotron.ca\nhttp://pages.infinit.net/shaun/");
}


D:\COM_CHM\Documentation CHM\_ASM\ASM__DOC\Tasm\ASMCHAP6.rtf

CHAPITRE 6 - Programme 100% Assembleur

L'effet de feu que j'ai présenté dans la section effets est réalisé en C. Évidemment, cela simplifie la compréhension. Mais qu'en est-il de la performance et de la taille du code? Pour ce qui est de la vitesse, rien d'extraordinaire. La taille? Environ 37820 octets. Pas si mal, non? Non. En fait c'est carrément minable. Vous allez comprendre après avoir vu une routine de feu 100% assembleur.

Optimisations

Il y quelques années encore, l'utilisation de l'assembleur était impératif. On ne pouvait pas concevoir d'écrire une demo ou un jeu dans un langage de haut niveau. Mais depuis quelques années, les compilateurs se sont grandement amélioré, et le besoin d'optimiser s'est quelque peu estompé. La raison en est simple: coder en assembleur demande du temps. Souvent, sur le marché, le temps de création d'un jeu est vital, surtout quand on compétionne avec d'autres concurrents pour un jeu du même genre. Par exemple, en 1993, après la sortie de Doom, une compagnie avait voulu sortir un jeu de raycasting similaire mais complètement en assembleur. Le jeu roulait a 35 fps sur un vieux 80286, certes. Mais le jeu ne vit jamais le jour et la compagnie fit faillite. Telle est la réalité du monde du développement de jeu (et le monde et général)... le temps est de l'argent.

Par mon expérience, je crois que les parties qui mérite d'être optimisé sont surtout les boucles internes, ce qui est appelé à chaque trame du programme, par exemple:

  • Les rendeurs de polygones
  • Le texture mapping
  • Routines de "blit" (transfert de mémoire vidéo)

Mais encore, rien ne peut remplacer de la bonne logique de programmation. Aussi puissant que le compilateur peut être, il n'est pas meilleur que son programmeur. Il est possible qu'un débutant crée du code assembleur plus lent que le compilateur aurait généré. Il faut donc bien analyser la logique de l'algorithme avant de convertir le tout en assembleur. Souvent l'algorithme peut être accélérer en "déroulant les boucles", ou en changeant carrément d'approche. Regardons à présent les avantages que présente un programme coder entièrement en assembleur.

Feu.com

Taille : 182 octets

Vraiment impressionnant, non? J'ai tout simplement traduit l'algorithme de feu présenté auparavant, avec quelques modifications. C'est sur ces quelques détails que je vais apporter votre attention. Mon premier obstacle fût de trouver une fonction de nombre aléatoire en assembleur. Heureusement, une petite recherche sur Internet m'en retourne une bonne dizaine. J'en choisi une, celle qui me semble la plus simple et directe. Elle retourne une valeur aléatoire dans DX:

; Procédure nombre aléatoire
Random PROC
MOV AX,[RandSeed]
MOV DX,8405h
MUL DX
INC AX
MOV [RandSeed],AX
RET
Random ENDP

Comme nous voulons des valeurs entre 0 et 255, nous n'utiliserons que la partie Low de DX, soit DL. Merveilleux, non? Cette fonction nous permets de créer le "foyer" du feu, c'est à dire la ligne inférieur du feu. L'algorithme de feu reste le même. Pour déterminer la couleur d'un pixel, on fait la moyenne des 4 pixels environnants, soit x+1, x-1, y-1 et le pixel x,y lui-même.

MOV AL, DS:[SI] ; x+(x-1)+(x+1)+(y+320)/4
ADD AL, DS:[SI+1] ; x-1
ADC AH, 0
ADD AL, DS:[SI-1] ; x+1
ADC AH, 0
ADD AL, DS:[SI+320] ; y-1
ADC AH, 0
SHR AX,2 ; division par 4

ADC nous permets de faire une addition avec la retenu, car si nos valeurs dépasse AL (255), ce registre ne suffit plus à contenir la variable. L'excédant se retrouve dans le carry, que ADC nous permets d'ajouter au total. Au moment d'afficher nos pixels, nous attendons d'abord une retrace verticale. Sans elle, le feu est vraiment rapide mais beaucoup moins esthétique. Par contre, la taille du feu descends à 160 octets. Je préfère cependant la beauté d'un effet à la vitesse absolue.

La copie du tableau de DS:SI vers ES:DI (Data sur Screen, ES vers A000h) s'effectue avec l'instruction MOVSD, Move String Double, qui déplace 1 double (soit 2 word, ou 4 octets) de... DS:SI vers ES:DI. Même si la dernière ligne est calculée, je ne l'affiche pas, car les valeurs aléatoires y sont générés, ce qui donne une ligne "hétérogène". J'ai commenté le code de A à Z, donc il ne devrait pas avoir de problèmes.

L'assembleur est un outil indispensable pour réaliser les effets les plus spectaculaires. J'insiste cependant pour que vous vous procuriez un livre sur le sujet, car ce petit cours n'est qu'une brève introduction sur ce vaste sujet, trop souvent oublié par les programmeurs de haut niveau.


Le cavalier hamiltonien

  • Forums
  • Tutoriels
  • Magazine
  • FAQs
  • Blogs
  • Projets
  • Chat
  • Newsletter
  • Études
  • Emploi
  • Club
  • Contacts
  • Accueil Actualités IT Pro
  • ALM Cycle de vie du logiciel
    • ALM
    • Merise
    • UML
  • Java Plateforme et langage Java
    • Java
    • Java Web
    • Spring
    • Android
    • Eclipse
    • NetBeans
  • .NET Microsoft Framework .NET
    • Microsoft DotNET
    • Visual Studio
    • ASP.NET
    • C#
    • VB.NET
    • Windows Phone
    • Microsoft Azure
  • Dév. Web Développement Web et Webmarketing
    • Développement Web
    • AJAX
    • Apache
    • ASP
    • CSS
    • Flash / Flex
    • JavaScript
    • PHP
    • Ruby & Rails
    • Web sémantique
    • Webmarketing
    • (X)HTML
  • EDI Environnements de Développement Intégré
    • EDI
    • 4D
    • Delphi
    • Eclipse
    • LabVIEW
    • NetBeans
    • MATLAB
    • Scilab
    • Visual Studio
    • WinDev
    • Visual Basic 6
    • Lazarus
    • Qt Creator
  • Programmation Programmation et langages de programmation applicatifs
    • Programmation
    • Débuter - Algorithmique
    • 2D - 3D - Jeux
    • Assembleur
    • C
    • C++
    • Objective C
    • Pascal
    • Perl
    • Python
    • Qt
    • XML
    • Autres
  • SGBD Systèmes de Gestion de Bases de Données
    • SGBD & SQL
    • 4D
    • Access
    • DB2
    • Firebird
    • InterBase
    • MySQL
    • NoSQL
    • PostgreSQL
    • Oracle
    • Sybase
    • SQL-Server
  • Office Bureautique pour l'entreprise
    • Microsoft Office
    • Access
    • Excel
    • Word
    • Outlook
    • PowerPoint
    • SharePoint
    • Microsoft Project
  • Solutions d'entreprise Autres logiciels pour l'entreprise
    • Solutions d'entreprise
    • Business Intelligence
    • ERP / PGI
    • CRM
    • SAS
    • Cloud Computing
    • SAP
    • Microsoft BizTalk Server
    • Talend
  • Applications Applications logicielles
    • Applications
    • Libres & Open Source
    • OpenOffice & LibreOffice
    • Projets
  • Mobiles Logiciels et matériels mobiles
    • Mobiles
    • Android
    • iOS
    • Windows Phone
  • Systèmes Logiciels et matériels systèmes
    • Systèmes
    • Windows
    • Linux Professionnel
    • Sécurité
    • PC
    • Mac
    • Réseau
    • Green IT
    • Virtualisation
    • Systèmes Embarqués
  • Programmation
  • Algorithmique
  • 2D-3D-Jeux
  • Assembleur
  • C
  • C++
  • Objective C
  • Pascal
  • Perl
  • Python
  • Qt
  • XML
  • Autres
  • Accueil C
  • Forums C
  • FAQ C
  • Tutoriels C
  • Livres C
  • Compilateurs et outils C
  • Sources C
  • Bibliothèques C
  • GTK+

Le cavalier hamiltonien

Table des matières Plier Déplier

  • I. Le cavalier hamiltonien : explication et mise en œuvre
  • II. Programme complet

Tutoriel pour C++ Builder

C++ pour tout ce qui est graphique, assembleur pour le calcul pur.

Article lu fois.

Les deux auteurs

Gilles Louïse Site personnel

Smortex

L'article

Publié le 8 octobre 2001 - Mis à jour le 14 janvier 2013

Version PDF Version hors-ligne

ePub, Azw et Mobi

Liens sociaux

Google Bookmarks ! Share on Google+

I. Le cavalier hamiltonien : explication et mise en œuvre▲

On appelle chemin hamiltonien, du nom du grand mathématicien Hamilton, un chemin particulier qui dans une avancée couvre l'ensemble des points en présence sans repasser par une position visitée. Ce sera le cas du cavalier qui chevauche l'ensemble de l'échiquier sans passer deux fois par la même case.

L'algorithme est très simple : il suffit de choisir à chaque fois le coup qui offre le plus petit nombre de possibilités différent de zéro (car zéro correspond au cul-de-sac). Ce principe est tellement puissant que le cavalier trouve tout de suite une première solution sans avoir à faire marche arrière, donc sans se tromper, quelle que soit la case de départ choisie (à quelques rares exceptions près e.g. la case A3 où la première solution n'est pas immédiate mais très rapide quand même). Ceci peut se comprendre intuitivement. Il faut attirer le cavalier vers les coins. En effet, à partir de la case A1 par exemple, il n'y a que deux possibilités, la case C2 et la case B3. Si donc le cavalier arrive par exemple en C2, il faut absolument qu'il choisisse A1, il ressortira alors en B3 alors que si, de C2 le cavalier opte pour une autre direction, la case A1 risque de passer définitivement aux oubliettes. Ainsi le cavalier a tendance à tourner autour de l'échiquier en privilégiant le choix des coins. Ayant trouvé une solution, le cavalier recule et essaie les autres chemins historisés qu'il n'a pas pris. Mais comme toute la structure de base est correcte puisque ce principe est adopté à chaque coup, le cavalier avance à nouveau facilement et trouve bien d'autres solutions.

L'échiquier est une zone de 120 octets, 12x10. Les cases de l'échiquier sont entourés de deux couches de 0xff (255 en décimal) qui est le code hors échiquier. Deux couches sont nécessaires à cause précisément du cavalier, en cas de sortie de l'échiquier, il pointe nécessairement une case dont le contenu est 255. Cela évite tout simplement de faire les quatre tests concernant x et y, x>=1 et x<=8 et y>=1 et y<=8. En un seul test, on sait si le cavalier est hors échiquier ou non. Cela dit, dans le cadre de ce programme, on testera seulement 0 ou non, 0 signifiera case libre et non 0 signifiera case non accessible (soit parce qu'elle est hors échiquier, soit parce qu'elle est déjà utilisée).

Sélectionnez
Echiquier db 255,255,255,255,255,255,255,255,255,255
          db 255,255,255,255,255,255,255,255,255,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255,255,255,255,255,255,255,255,255,255
          db 255,255,255,255,255,255,255,255,255,255

Nous décidons que la case A1 se trouve à l'adresse Echiquier+21, soit la première case nulle à partir de l'adresse Echiquier. Si esi pointe une case de l'échiquier, ajouter 10 nous fait avancer d'une case verticalement (rangée), soustraire 10 nous fait reculer d'une case verticalement, ajouter 1 nous fait avancer d'une case horizontalement (colonne), soustraire 1 nous fait reculer d'une case horizontalement. On déclare comme suit les huit directions du cavalier.

Sélectionnez
DirC dd      8, -2,  1
     dd     12,  2,  1
     dd     19, -1,  2
     dd     21,  1,  2
     dd     -8,  2, -1
     dd    -12, -2, -1
     dd    -19,  1, -2
     dd    -21, -1, -2

C'est un double tableau, chaque direction tient sur trois nombres de 32 bits (donc 12 octets pour une direction). Le premier nombre indique l'offset du point de vue de l'échiquier, les deux autres nombres correspondent aux offsets en x et y et seront utilisés pour les calculs. Ces deux notations se rejoignent puisque 10 fois le troisième nombre plus le deuxième donne le premier, c'est pour éviter d'avoir à recalculer sans cesse la même chose que nous mettons ces valeurs à notre disposition. Ainsi, si esi pointe l'échiquier, on testera le contenu de esi+valeur1 (offset d'échiquier) pour une direction donnée. On déclare une zone de 128 octets qui contiendra le chemin emprunté par le cavalier, c'est une suite de 64 couples (x,y).

Sélectionnez
char Chemin[128];

Du fait qu'on utilisera cette zone dans le code C++ par une syntaxe du type Chemin[i] pour écrire dans le bitmap hors écran le numéro des coups dans les cases graphiques, cette déclaration se fait en C++ et non en assembleur. Chaque couple contiendra les coordonnées (x,y) de la case, x et y étant compris entre 0 et 7. Ainsi (0,0) correspondra à la case A1 et (7,7) à la case H8. Le premier nombre désigne le nombre de colonnes à ajouter horizontalement et le second le nombre de cases à ajouter verticalement. Ce choix est arbitraire, nous respectons simplement la notation algébrique des cases. Le premier couple contiendra la case de départ (x,y). Quand un coup sera choisi, il correspondra obligatoirement à une des huit directions possibles. Comme chaque direction donne les offsets en x et y, on ajoutera tout simplement ces offsets pour obtenir la nouvelle position du cavalier. Par exemple, imaginons que le cavalier soit sur la case D2 soit (3,1) et que le coup choisi corresponde à la direction -8, on voit que les offsets correspondant à -8 sont 2 et -1 (car -1*10+2=-8), donc pour obtenir la case qui sera écrite dans la zone Chemin, on ajoutera les offsets respectifs en x et y à la position courante soit 3+2=5 et 1-1=0, la case est donc (5,0) soit F1. Ensuite, on écrira un 1 dans la zone Echiquier correspondant à cette case. Comme NbCoups est égal au nombre de coups présents dans Chemin, on calculera d'abord l'adresse Chemin+NbCoups*2 qui contient sur deux octets consécutifs la case où se trouve pour l'instant le cavalier. On lira le contenu à adresse suivante qui contient l'offset en y, on multipliera par 10, on ajoutera le contenu de la case précédente qui contient l'offset en x. Ce calcul équivaut à 10y+x. On obtient ainsi l'offset absolu à ajouter à Echiquier+21 pour obtenir la case de l'échiquier correspondante, on y écrira un 1. Au moment d'un recul, on procédera au même calcul à cela près qu'on écrira 0 dans la case de l'échiquier, ce qui aura pour effet de libérer la case. On déclare une zone historique constitué de 63 fois LigHisto octets.

Sélectionnez
Histo db LigHisto*63 dup(?)

LigHisto (ligne d'historique) est une constante égale à 33. Pour chaque coup, il y aura donc une zone historique de LigHisto soit 33 octets soit au maximum, huit fois une série de quatre octets plus un code fin 255 (8*4+1=33). Le premier octet de chaque série sera l'offset calculé comme possible, les deux suivants les offsets en x et y pour nous éviter d'avoir à en faire la recherche et le dernier octet de ces séries de quatre sera le nombre de coups possibles disponibles si le coup de cette série était joué. La méthode est donc simple. À partir de la case de départ, on cherche tous les coups possibles et on les historise ainsi. Ensuite, on cherche dans l'historique le coup associé à un nombre (le quatrième nombre de chaque série de quatre octets) minimum non nul, on joue ce coup considéré comme le meilleur et on le supprime de l'historique pour qu'il ne soit pas rejoué si le pointeur historique retombe à ce niveau-là. Pour supprimer un coup de l'historique, il suffit de mettre son offset à 0 c'est-à-dire le premier octet de la série à annuler. On continue ainsi. Quand on arrive à un cul-de-sac (ou à une solution qui est un cul-de-sac particulier), on recule en cherchant dans l'historique s'il y a d'autres possibilités. Si non, on recule encore jusqu'à en trouver. Si on en trouve, on est certain que le coup choisi sera différent étant donné que les coups joués sont effacés de l'historique. Le cavalier avance donc à nouveau avec un nouveau coup non encore essayé jusqu'à nouvelle solution ou cul-de-sac. Par cette méthode, on épuise toutes les combinaisons possibles.

Le dialogue entre le C++ (graphique) et l'assembleur (calcul) se fait via les variables déclarées en C++. Quand une variable est déclarée en C++, elle est accessible à l'assembleur mais la réciproque est plus difficile voire fausse, les zones déclarées en assembleur ne sont accessibles qu'au seul assembleur (ou alors il faudrait user de syntaxes ou de techniques inhabituelles en C++ et inutiles). C'est pourquoi nous avons déclaré trois zones mémoire en assembleur, l'échiquier, les directions du cavalier et l'historique car ces zones n'ont pas besoin d'être accédées par le code C++. En revanche, le chemin choisi par le cavalier est déclaré par une déclaration C++ de type char[] car le C++ a besoin de lire cette zone pour convertir les coordonnées (x,y) de passage du cavalier en coordonnées graphiques d'affichage. Dans les variables générales (situées après les "include" et avant la toute première fonction qui est par défaut le constructeur TForm1::TForm1), on a déclaré successivement le pointeur de TForm1 (en fait c'est C++Builder qui a écrit lui-même cette ligne dans la partie créée automatiquement), le pointeur de bitmap BM, la zone Chemin, une zone de trois caractères (deux caractères et un code fin) qui contiendra la case de départ, le nombre de coups, le nombre de solutions et un pointeur sur l'historique.

Sélectionnez
TForm1 *Form1;
Graphics::TBitmap *BM;
char Chemin[128];
char Depart[3];
char NbCoups;
int NbSol;
char *pHisto;

L'affichage est d'abord créé dans un bitmap donc en hors écran. Ce bitmap est créé par new dans le constructeur de TForm1, on utilise généralement ce constructeur pour initialiser l'application. On initialise ses dimensions à celles de l'écran avec Screen->Width/Height.

Sélectionnez
BM=new Graphics::TBitmap();
BM->PixelFormat=pf4bit;
BM->Width=Screen->Width;
BM->Height=Screen->Height;

C'est un bon principe de dimensionner au maximum un bitmap hors écran, cela évite d'avoir à modifier ses dimensions en cas de resize de la fenêtre. On n'utilise qu'un bitmap à quatre bits par pixel (pf4bit), ce qui est suffisant pour notre problème, l'affichage n'en sera que plus rapide. C'est dans la méthode OnActivate de Form1 que le programme va réellement se dérouler. On commence par demander à l'utilisateur d'entrer la case de départ.

Sélectionnez
Rep=0;
while(!Rep)
{
  /* entrée de la case de départ */
  CasDep = InputBox(Msg[0],Msg[1],"");

  /* si rien, Rep=1 */
  if (!CasDep.Length()) Rep=1;

  /* si quelque chose, test de la validité de la saisie */
  if (  CasDep.Length()==2
     && CasDep[1]>='A'
     && CasDep[1]<='H'
     && CasDep[2]>='1'
     && CasDep[2]<='8') Rep=2;
}

La variable CasDep est un AnsiString local à la méthode OnActivate. On boucle jusqu'à ce que l'utilisateur ait entré quelque chose de correct, soit rien auquel cas Rep=1 soit quelque chose de correct, une lettre entre A et H et un chiffre entre 1 et 8 auquel cas Rep=2. Sinon, si Rep est toujours à zéro après cette tentative, la saisie n'est pas correcte et la boucle en while continue, on redemande donc à l'utilisateur une entrée correcte. Au sortir de cette boucle en while, de deux choses l'une, ou bien Rep=1 auquel cas l'utilisateur n'a rien saisi et l'on sort simplement de l'application sinon Rep=2 et le calcul va commencer.

Sélectionnez
if(Rep==1) Close(); else Cavalier(CasDep,Sender);

La fonction Cavalier reçoit en argument l'AnsiString qui contient la case de départ et le Sender nécessaire car il faudra appeler la méthode OnPaint de Form1, cette méthode a besoin de connaître le Sender. Comme CasDep a maintenant deux caractères corrects, on recopie cet AnsiString dans la variable générale Depart.

Sélectionnez
strcpy(Depart,CasDep.c_str());

À noter la méthode très utile c_str() qui convertit un AnsiString en char* à zéro terminal. Dans ces conditions, Depart[0] sera la lettre compris entre A et H et Depart[1] sera le chiffre compris entre 1 et 8. On appelle le sous-programme assembleur CalOCD qui calcule les offsets en x et en y de la case de départ et les enregistre au début de la zone Chemin. On lit dans le registre 8 bits al la première case Depart[0] (mov al,Depart), on soustrait 65 qui est le code ascii de la lettre A (sub al,65), al contient alors une valeur entre 0 et 7, 0 pour A, 1 pour B etc. puis enfin on écrit le résultat dans la toute première case de la zone Chemin (mov Chemin,al). Puis on lit dans al le chiffre Depart[1] (mov al,Depart+1), on soustrait 49 qui est le code ascii du chiffre 1 (sub al,49), al contient donc une valeur compris entre 0 et 7, 0 pour 1, 1 pour 2 etc. puis on enregistre le résultat al dans la deuxième case de la zone Chemin (mov Chemin+1,al). On termine par ret (return) comme tout sous-programme, on retourne ainsi à l'appelant. Donc, après exécution de ce sous-programme, Chemin contient au début la case choisie par l'utilisateur entre (0,0) et (7,7).

Sélectionnez
CalOCD:      mov al,Depart
             sub al,65
             mov Chemin,al
             mov al,Depart+1
             sub al,49
             mov Chemin+1,al
             ret

La case de départ étant maintenant renseignée dans Chemin, on peut afficher l'état de l'échiquier qui contiendra le chiffre 1 dans cette case, on dessine donc le bitmap

Sélectionnez
Dessine(S);

où S est le Sender de type TObject* nécessaire car cette fonction va appeler le OnPaint de Form1 (fenêtre principale) dès que le bitmap aura été dessiné. La suite de la fonction Cavalier se constitue d'une boucle générale contenant deux moments, l'avance en boucle et le recul en boucle. Le schéma algorithmique général de cette boucle est

Sélectionnez
do
   {
   avance jusqu&#146;à cul-de-sac
   recule jusqu&#146;à réavance possible
   }
while(NbSol !=Solutions) ;

où Solutions est une constante déclarée en début de programme. Nous l'avons positionnée à 10 donc le programme affichera les dix premières solutions. On voit donc que la boucle générale inclut elle-même deux sous-boucles, une sous-boucle d'avance et une sous-boucle de recul. La sous-boucle d'avance se constitue de deux parties, la première en assembleur qui cherche et joue le meilleur coup et la deuxième qui dessine et affiche le coup.

Sélectionnez
do
  {
    asm
    {
        call Joue
        mov AvanceOK,dh
    }
    Dessine(S);
  }
while(AvanceOK) ;

Le sous-programme Joue renvoie dans dh une indication, c'est une sorte de flag. La position de ce flag est copiée dans la variable AvanceOK et le while de la boucle décidera s'il faut continuer à avancer ou non. Si dh<>0 (true) on continuera, si dh=0 (false) on arrêtera la boucle en while et ce, parce qu'on est tombé sur un cul-de-sac ou une solution. Après cette avancée maximale, on interroge la variable NbCoups. Si NbCoups est égal à 63, cela signifie qu'une solution a été trouvée, on affiche simplement un MessageBox, ce qui nous laissera le temps de consulter la solution à l'écran.

Sélectionnez
if(NbCoups==63)
    {
    AnsiString A;
    A="Solution numéro "+IntToStr(++NbSol);
    Application->MessageBox(A.c_str(),"Solution",MB_OK);
    }

Ensuite, on positionne la variable booléenne ReculeOK en fonction du fait qu'on a atteint le nombre de solutions demandées ou non.

Sélectionnez
ReculeOK=(NbSol != Solutions)

ReculeOK sera vrai si on n'a pas atteint le nombre de solutions à afficher, la variable autorisera alors le recul. Cette méthode permet d'avoir une solution complète à l'écran au moment où le programme s'arrêtera, si on ne prenait pas cette petite précaution, nous n'aurions qu'une solution partielle affichée à la fin de l'exécution de la méthode OnAvtivate. Nous nous arrêtons après avoir affiché un nombre arbitraire de solutions car il y a probablement des milliards de solutions pour une case de départ donnée, le programme ne s'arrêterait jamais. Dans ces conditions, le recul se passe comme suit :

Sélectionnez
while(ReculeOK)
  {
    asm
    {
        call Recule
        mov ReculeOK,al
    }
    Dessine(S);
  }

On utilise une structure en while car il se peut que nous ne reculions pas. C'est ce qui se produit quand la dernière solution est trouvée (car on n'affiche qu'un petit nombre de solutions, voir constante Solutions en en-tête du cpp), on empêche ce recul de manière à avoir une solution complète d'affichée (alors que pour l'avance nous avons utilisé une structure en do car nous étions sûrs d'avoir à avancer au moins une fois). De la même façon que pour l'avance, le sous-programme assembleur Recule renvoie une indication mais dans al qui dira s'il faut continuer le recul ou non. Si al<>0 (true), on continuera le recul parce qu'aucun coup n'a été trouvé dans le groupe historique correspondant au numéro de coup traité NbCoups. Si al=0 (false), l'historique pour ce coup n'est pas vide, il existe donc au moins un coup à jouer, on cesse donc de reculer pour jouer un coup résiduel de l'historique. À chaque fois, on appelle la fonction Dessine, ce qui aura pour effet de vider la case libérée par le recul. En effet, bien que les coups joués restent dans la zone Chemin (ils ne sont pas effacés de cette zone suite à un recul), la seule décrémentation de NbCoups suffit à faire reculer graphiquement le cavalier puisque le graphique se redessine chaque fois à partir de rien. On sait qu'il y a à tout moment NbCoups+1 coordonnées dans la zone Chemin. Le graphique consiste à écrire les nombres allant de 1 à NbCoups+1 dans les cases visées. Si NbCoups est décrémenté, le dernier nombre n'apparaît plus, cela donne donc l'impression d'un recul du cavalier.

Voici comment nous recherchons les coups possibles dans une position donnée. Deux variables sont importantes, NbCoups et la zone Chemin. En effet, à l'adresse Chemin+NbCoups*2 se trouve la position du cavalier et l'on va enregistrer les coups dans l'historique à partir de l'adresse Histo+NbCoups*LigHisto, c'est l'adresse de la première case du groupe de LigHisto cases correspondant au coup numéro NbCoups. Première chose, on calcule cette adresse, on fait pointer esi à l'adresse Histo (lea esi,Histo), c'est l'adresse du tout premier groupe historique. Ensuite on charge dans eax NbCoups, comme NbCoups est sur 8 bits, on met d'abord eax à 0 (xor eax,eax) puis on charge dans al NbCoups (mov al,NbCoups), dans ces conditions eax=NbCoups sur 32 bits. Ensuite on multiplie al par LigHisto, pour ce faire on charge LigHisto dans cl (mov cl,LigHisto) puis on multiplie al par cl (mul cl), à ce stade ax=LigHisto*NbCoups (résultat sur 16 bits) mais comme eax a été remis à 0 au début, on a eax=LigHisto*NbCoups. On ajoute alors eax à esi (add esi,eax), comme esi pointait Histo, esi pointe bien maintenant Histo+NbCoups*LigHisto c'est-à-dire le bon groupe historique où l'on va stocker les coups possibles. On sauvegarde esi dans la variable pHisto (mov pHisto,esi) pour ne pas avoir à la recalculer en fonction de NbCoups. On écrit le code fin 255 dans l'historique (mov byte ptr [esi],255). La précision byte ptr (pointeur d'octet) est nécessaire pour une valeur numérique car il faut préciser s'il s'agit de 255 sur 8 bits (byte ptr), sur 16 bits (word ptr) ou sur 32 bits (dword ptr). En revanche quand on écrit ou lit un registre, on ne fait pas cette précision car le compilateur sait bien que al est sur 8 bits, ax sur 16 bits et eax sur 32 bits et idem pour tout autre registre e.g. bl, bx, ebx etc. Ensuite on va écrire un 1 dans la case pointée par le cavalier de manière à ce que durant l'analyse on puisse savoir que cette case est inaccessible et on va en profiter pour renvoyer dans esi l'adresse correspondante dans l'échiquier. On met donc dh à 1 (mov dh,1) et on écrit dh dans la bonne case (call Ecritdh), Ecritdh étant une petite routine qui se charge de ce travail. À ce stade, esi pointe la case où se trouve le cavalier (calculé par Ecritdh), cette case contient un 1. Notre sous-programme Cherche va renvoyer dh à l'appelant. Si dh<>0, cela signifie qu'un coup au moins est jouable, si dh=0 cela signifie qu'aucun coup n'a pu être historisé à ce stade. On initialise donc le flag ou le témoin dh à 0 (xor dh,dh). Ensuite on fait pointer edi sur les différentes directions que le cavalier peut prendre (lea edi,DirC) et on met le compteur cl à 8 car cette table contient 8 directions à tester (mov cl,8). La boucle de recherche peut commencer. On lit dans ebx la direction à tester (mov ebx,[edi]). N'oubliez pas que les 3 valeurs pour chaque direction sont sur 32 bits, c'est une petite astuce pour faciliter la lecture, ebx est une valeur signée sur 32 bits. Comme esi pointe la case du cavalier et que ebx contient l'offset d'une direction parmi les huit possibles, esi+ebx représente une des cases possibles où le cavalier peut se rendre, donc on lit le contenu de cette case dans al (mov al,[esi+ebx]) puis on regarde si ce contenu est nul ou non (test al,al), test étant un ET logique non destructif en l'occurrence ici un ET logique du registre al avec lui-même. Ici si ZF=1 alors al=0 (la case est vide), si ZF=0 alors al<>0 et la case n'est pas vide. Notez que si ZF=0, on ne sait pas si cette case est hors échiquier (code 255) ou bien si cette case a déjà été piétinée par le cavalier (code 1) mais cela ne nous importe peu, ce qu'on sait si ZF=0, c'est que cette case n'est pas possible et cela nous suffit. Si la case n'est pas vide (ZF=0), on saute à l'adresse Cherche20 pour continuer à scruter les directions (jnz Cherche20), instruction qui signifie "saute à l'adresse Cherche20 si ZF=0", jnz=jump si non Z ou ce qui revient au même "jmp si ZF=0". Si ce saut n'a pas lieu, alors la case testée est vide, on va donc se poser la question de savoir combien de cases nous aurions à notre disposition si on jouait ce coup, on appelle pour ce faire le sous-programme Combien qui fait ce calcul (call Combien). Ce sous-programme enregistre dans l'historique le coup possible et lui associe un nombre qui sera le nombre de coups qu'on aurait si on jouait ce coup. Ce sous-programme met aussi dh à 1 pour signifier à l'appelant Cherche qu'on a trouvé au moins un coup. À chaque appel de Combien, le groupe historique visé s'enrichit d'une nouvelle possibilité. À ce stade, que la case ait été possible ou non, on a testé la direction pointée par edi, on pointe alors la direction suivante (add edi,12). On a vu que chaque direction est constituée de trois mots de 32 bits soit 12 octets, il faut donc ajouter 12 à edi pour qu'il pointe la direction suivante. On décrémente le compteur cl (dec cl), si cl=0 après cette décrémentation on aura ZF=1 et cela signifiera qu'on a testé toutes les directions, si cl<>0 on aura ZF=0, il reste alors des directions à tester. Si donc ZF=0, on doit continuer (jnz Cherche10), saute à l'adresse Cherche10 s'il reste des directions à tester, comme edi pointe la direction, cette instruction signifie "saute pour tester maintenant la direction pointée par edi". Sinon, si ZF=1, le saut n'aura pas lieu, toutes les directions ont été testées, on retourne à l'appelant (ret). On retourne avec dh qui dira si oui ou non on a trouvé un coup, dh a été initialisé à 0 avant la boucle qui scrute les directions, si le sous-programme Combien n'est jamais appelé, dh restera à 0, le retour se fera donc avec dh=0 et l'appelant conclura qu'il n'y a pas de coup. Si le sous-programme Combien est appelé au moins une fois, on aura dh=1 car Combien se charge de mettre dh à 1, le retour se fera donc avec dh=1 et l'appelant conclura qu'il existe au moins un coup jouable dans l'historique.

Sélectionnez
Cherche:     lea  esi,Histo          // esi sur historique
             xor  eax,eax            // eax=0
             mov  al,NbCoups         // eax=NbCoups
             mov  cl,LigHisto        // cl=nb d&#146;octets par ligne histo
             mul  cl                 // eax=NbCoups*LigCoups
             add  esi,eax            // esi sur bon groupe histo
             mov  pHisto,esi         // mémorisé dans pHisto
             mov  byte ptr [esi],255 // code fin au début de ce groupe
             mov  dh,1               // on va écrire 1 dans la case
             call Ecritdh            // la case est occupée
             xor  dh,dh              // dh=0 flag de coup trouvé
             lea  edi,DirC           // table des directions
             mov  cl,8               // 8 directions possibles
Cherche10:   mov  ebx,[edi]          // ebx = une des directions
             mov  al,[esi+ebx]       // al=contenu de cette case
             test al,al              // case vide?
             jnz  Cherche20          // non on passe à la suivante
             call Combien            // oui coups possibles
Cherche20:   add  edi,12             // direction suivante
             dec  cl                 // fin?
             jnz  Cherche10          // non refelemele
             ret                     // renvoie dh flag de coup trouvé

Voyons maintenant ce que va faire le sous-programme Combien. Il est appelé parce qu'une direction est reconnue comme possible et ce, parce que le contenu de la case esi+ebx est nul, esi étant la position du cavalier et ebx la direction testée parmi les huit possibles. Combien va faire comme si le cavalier se trouvait à la position esi+ebx et va calculer le nombre de directions qu'il aurait à sa disposition si ce coup était joué. Puis, il va enregistrer d'une part la direction testée soit bl sur huit bits, le nombre de coups disponibles calculé dans la boucle mais aussi les offsets x et y correspondant à la direction bl situés aux adresses edi+4 et edi+8 (on ne lit que le LSB des valeurs, cela nous suffit). Notons que même si le nombre de coups possibles est zéro, on enregistre quand même cette information. La raison en est que l'avant-dernier sera tel que le coup suivant sera un cul-de-sac, le nombre enregistré associé sera donc zéro mais ce coup nous est utile. C'est bien entendu un choix algorithmique, on pourrait faire en sorte de ne pas enregistrer ce genre de coup mais dans ce cas il faudrait tester NbCoups, l'algorithme serait quelque peu différent. En arrivant à Combien, il existe trois éléments à ne pas perdre qui sont esi qui pointe la position du cavalier sur l'échiquier, cl qui sert de compteur de directions dans Cherche et edi qui pointe la direction testée dans le tableau de directions DirC. Quant à ebx, il contient le direction testée mais cet élément n'est pas sauvegardé, l'appelant n'en ayant plus besoin. On commence donc par sauvegarder la position du cavalier (push esi), le compteur de directions (push ecx) et le pointeur de directions (push edi). On sauvegarde edi en dernier car avant le retour il faudra lire les offsets x et y pour les écrire dans l'historique et donc pouvoir dépiler par l'instruction pop le registre edi pour le refaire pointer sur la direction testée. Chaque groupe historique se constitue comme nous l'avons dit de quatre octets, l'offset d'échiquier, l'offset en x, l'offset en y et le nombre de coups possibles associé à ce coup. La sauvegarde des offsets x et y n'a qu'une seule vocation, éviter d'avoir à les rechercher dans DirC (ce qui serait possible car à tout bl de DirC correspond un couple unique x et y). Ensuite on fait aller le cavalier sur la case à tester, pour ce faire on ajoute ebx à esi (add esi,ebx) car ebx contient la direction testée. On fait pointer edi sur les huit directions possibles à tester (lea edi,DirC), on charge dans cl le nombre de directions (mov cl,8), on met ch à 0 (xor ch,ch), ch va compter le nombre de coups possibles à partir de la nouvelle position du cavalier que pointe esi. La boucle de recherche peut maintenant commencer. On charge dans ebx la direction à tester (mov ebx,[edi]), on lit dans al le contenu de la case esi+ebx (mov al,[esi+ebx]) puis on regarde si ce contenu est nul ou non (test al,al). Si al n'est pas nul, il s'agit soit d'une case hors échiquier soit d'une case déjà visitée, dans ce cas, la direction testée n'est pas jouable, on saute à Combien20 (jnz Combien20). Si ce saut n'a pas eu lieu, c'est que ZF=1 et donc que al=0, la case est alors vide, on pourrait y aller, donc on incrémente le compteur ch de coups acceptables (inc ch). On voit que le test d'une direction n'a qu'un seul but, incrémenter ou non ch. Que ch ait été ou non incrémenté, on fait pointer edi à la direction suivante, on se souvient que chaque direction est codifiée sur 12 octets (add edi,12), on décrémente le compteur de directions (dec cl). Si après cette décrémentation ZF=0, alors cl n'est pas encore nul en conséquence de quoi il reste des directions à tester, on se branche donc à Combien10 pour tester la direction suivante (jnz Combien10). Si ce saut n'a pas lieu, alors cl=0, on a alors passé en revue toutes les directions possibles et ch contient le nombre de directions acceptées. Nous allons donc maintenant enregistrer ces données dans l'historique. On fait pointer esi au bon groupe historique, il suffit de lire le pointeur pHisto (mov esi,pHisto). En effet, dans Cherche nous avons mémorisé cette adresse pour l'avoir à disposition et éviter de la recalculer, esi pointe donc le bon groupe historique. On se souvient que chaque série du groupe est de quatre octets et que le code fin est 255. On cherche donc à pointer le premier octet d'une série égal à 255, cela signifiera que cette série est libre pour y contenir un nouvel enregistrement historique. On lit donc dans al le premier octet d'une série (mov al,[esi]), puis on pointe la série suivante (add esi,4), on teste maintenant le contenu de al pour savoir s'il est égal à 255, on ne fait que l'incrémenter (inc al), c'est plus simple que de comparer avec 255. Si al n'est pas nul après cette incrémentation (car 255+1=0 sur 8 bits), alors al n'était pas égal à 255, donc esi ne pointait pas avant d'avancer la première série libre donc on continue le test en bouclant à Combien30 (jnz Combien30). Si ce saut n'a pas lieu, alors ZF=1, donc al=0 donc al était égal à 255 donc esi pointait avant d'avancer la première série libre. Comme esi pointe la série suivante, on sait qu'il faut écrire dans la série précédente qui est libre, esi-4 est l'adresse du premier octet de la série qui devra contenir la direction testée, esi-3 l'offset en x, esi-2 l'offset en y et esi-1 le nombre de coups autorisés. On écrit le nombre de coups trouvés jouables, ce nombre est le compteur ch (mov [esi-1],ch) et comme esi pointe la série suivante, on y écrit le code fin (mov byte ptr [esi],255), ce qui signifiera plus tard que cette série est libre pour enregistrer d'autres coups ou encore qu'on est arrivé à la dernière série pour ce groupe historique. Maintenant on récupère edi que nous avions sauvegardé en dernier (pop edi), edi pointe la direction testée dans Cherche, nous allons sauvegarder dans l'historique l'offset d'échiquier ainsi que les offsets en x et y correspondant à la direction. On lit dans al l'offset d'échiquier testé (mov al,[edi]), on ne lit que le LSB (l'octet le moins significatif, last significant byte) qui seul est informatif, les autres octets sont soit à 0 si la direction est positive soit à 255 si la direction est négative, on sauvegarde cet offset dans l'historique dans la première case de la série libre (mov [esi-4],al). On lit maintenant l'offset en x (mov al,[edi+4]) et on sauvegarde cet offset dans l'historique (mov [esi-3],al), enfin on lit l'offset en y (mov al,[edi+8]) et on le sauvegarde dans l'historique (mov [esi-2],al). Le sous-programme est terminé, il ne reste qu'à restituer le compteur ecx (pop ecx) et le pointeur esi (pop esi) qui contient l'adresse où se trouve le cavalier puis on rend la main à l'appelant (ret) à savoir à Cherche qui va pouvoir continuer sa boucle dans de bonnes conditions puisque edi est l'adresse de la direction qui vient d'être testée, esi l'adresse du cavalier et cl le compteur dans Cherche qui contrôle qu'on ne teste que 8 directions.

Sélectionnez
Combien:     push esi                  // position du cavalier en pile
             push ecx                  // compteur de directions en pile
             push edi                  // pointeur de directions en pile
             mov  dh,1                 // on a trouvé un coup
             add  esi,ebx              // esi sur cette case
             lea  edi,DirC             // table des directions
             mov  cl,8                 // 8 directions
             xor  ch,ch                // ch=0 compteur de coups acceptés
Combien10:   mov  ebx,[edi]            // ebx = une des directions
             mov  al,[esi+ebx]         // al=contenu de cette case
             test al,al                // case libre?
             jnz  Combien20            // non saute
             inc  ch                   // oui compteur+1 de coups possibles
Combien20:   add  edi,12               // direction suivante
             dec  cl                   // fin?
             jnz  Combien10            // non refelemele
             mov  esi,pHisto           // pHisto pointe déjà le bon groupe
Combien30:   mov  al,[esi]             // direction historique
             add  esi,4                // avance toujours
             inc  al                   // code fin?
             jnz  Combien30            // non continue jusqu'au code fin
             mov  [esi-1],ch           // nombre de coups jouables
             mov  byte ptr [esi],255   // code fin de la série historique suivante
             pop  edi                  // récupère la direction testée
             mov  al,[edi]             // al=direction testée
             mov  [esi-4],al           // dans l'historique
             mov  al,[edi+4]           // al=offset en x
             mov  [esi-3],al           // dans l'historique
             mov  al,[edi+8]           // al=offset en y
             mov  [esi-2],al           // dans l'historique
             pop  ecx                  // récupère le compteur
             pop  esi                  // récupère la position du cavalier
             ret                       // retour à l&#146;appelant

Le sous-programme Meilleur recherche dans le groupe historique correspondant au coup à traiter le meilleur coup c'est-à-dire selon notre algorithme le coup associé à un nombre minimal de possibilités futures. On se souvient que chaque série d'un groupe historique se constitue de quatre octets : offset, x, y, nombre. À tout offset correspond donc un nombre qui est le nombre de coups qu'aurait à sa disposition le cavalier si cet offset était joué. Meilleur cherche donc l'offset dont le nombre associé est le plus petit possible. Meilleur renvoie aussi le registre 8 bits dh à titre de flag qui indique s'il y a ou non un coup disponible dans le groupe historique concerné car il se peut que tous les coups aient été déjà joués, en conséquence de quoi le groupe historique est vide. Si dh<>0 (true), cela signifie qu'un coup a été trouvé dans l'historique et qu'il a été joué. Si dh=0 (false), Meilleur n'a trouvé aucun coup dans l'historique, le cavalier n'a donc pas bougé, on est dans un cul-de-sac. Notons le cas particulier du dernier coup. Si un coup est trouvé et si NbCoups=62 (62 correspond au dernier coup car NbCoups n'a pas encore été incrémenté, il le sera juste après dans le programme si un coup a été trouvé), alors on joue le coup, on sait qu'il est obligatoirement unique, on ne fait donc pas de recherche de minimum et on renverra quand même dh=0 bien qu'un coup à savoir le dernier a été joué. Cette petite astuce fera croire à l'appelant qu'on est arrivé à un cul-de-sac (ce qui est vrai, une solution étant un cul-de-sac particulier), en conséquence de quoi la boucle en while s'arrêtera. On évite ainsi d'avancer encore quand une solution a été trouvée. Le programme fonctionnerait quand même si on renvoyait dh=1, simplement il chercherait à avancer, ne trouverait rien et finirait par reculer. En renvoyant dh=0 pour le dernier coup, on simplifie la tâche au programme, la boucle en while s'arrêtera et on reculera pour trouver d'autres solutions. Meilleur commence donc par initialiser ce flag dh en le mettant à 0 (xor dh,dh). Ensuite on fait pointer esi dans le bon groupe historique (mov esi,pHisto). Ne confondez pas lea (load effective adresse) et mov. Quand l'adresse est connue, on utilise lea, par exemple pour pointer la case A1 on écrit "lea Echiquier+21". Mais quand on lit une adresse en mémoire, on utilise mov, par exemple ici "mov esi,pHisto", esi ne pointe pas pHisto, il contient l'adresse située à l'adresse pHisto (le désassemblage donne mov dans les deux cas mais avec les crochets pour [pHisto], cela montre clairement qu'on lit un pointeur en mémoire). Ensuite on initialise le minimum à un grand nombre (mov bl,255). En réalité, comme le nombre maximum de coups de sera 8 (8 la première fois et 7 pour tous les autres coups), n'importe quel nombre supérieur à 8 conviendrait pour cette initialisation. Si on trouve un coup, on comparera ce minimum au nombre situé dans la série historique. La première fois, ce nombre sera nécessairement inférieur à bl puisqu'on a initialisé le minimum au maximum mais pour les autres fois, cela dépendra des valeurs en présence. La boucle commence donc, on lit dans al l'octet pointé par esi à savoir l'offset de la série (mov al,[esi]). On incrémente ce nombre pour savoir s'il était égal à 255 qui est le code fin du groupe historique (inc al). Si ZF=1, cela signifie qu'on est à la fin du groupe historique, on l'a donc parcouru entièrement et l'on saute à l'adresse Meilleur30 (jz Meilleur30). Si ce saut n'a pas lieu, al n'était pas égal à 255, donc esi ne pointe pas la fin de l'historique. On décrémente al qui reprend donc sa valeur d'origine pour savoir s'il est nul (dec al). Si ZF=1 alors al=0, cela signifie que l'offset lu a été effacé, c'est un ancien coup qui a été joué et effacé pour éviter de le rejouer. Si ZF=1, on saute à l'adresse Meilleur20 (jz Meilleur20). Si le saut n'a pas lieu, cela signifie que al n'est ni égal à 255 (code fin) ni égal à 0 (coup effacé), donc esi pointe une série historique contenant un coup jouable. À ce stade, on cherche à savoir si NbCoups est égal à 62 (cmp NbCoups,62). Si ZF=0, cela signifie que NbCoups n'est pas égal à 62 c'est-à-dire qu'on n'est pas en train de chercher le tout dernier coup, on saute alors à l'adresse Meilleur15 qui est le cas général (jnz Meilleur15). Si ce saut n'a pas lieu, alors NbCoups=62, on sauvegarde dans edi l'adresse de la série historique correspondant au coup à jouer (mov edi,esi) puis l'on saute à Meilleur40 pour jouer ce coup (jmp Meilleur40). Notez que dh=0 dans ce cas, ainsi que nous l'avons déjà dit, cela provoquera l'arrêt de la boucle en while dans le C++. On se retrouve à l'adresse Meilleur15 dans le cas général à savoir un coup trouvé avec NbCoups différent de 62. On charge dans al le nombre de coups mémorisés dans cette série historique (mov al,[esi+3]), il s'agit bien de l'adresse esi+3 puisque esi+0=esi=adresse de l'offset, esi+1=adresse où il y a x, esi+2=adresse où il y a y et esi+3=adresse où il y a le nombre de coups possibles pour ce coup mémorisé. On regarde si ce nombre de coups est nul ou non (test al,al). Si oui, ZF=1 et l'on saute l'adresse Meilleur20 (jz Meilleur20) car ce coup ne nous intéresse pas puisqu'il représente un cul-de-sac. En effet, nous cherchons le minimum certes mais un minimum non nul. Si le saut n'a pas lieu, on tient là un vrai coup. On commence par mettre dh à 1 (mov dh,1), cela indiquera à l'appelant qu'on va jouer un coup puisqu'on est certain dans ce cas de pouvoir jouer. Ensuite, on cherche à savoir si al est inférieur à bl (cmp al,bl). Si CF=0, cela signifie que al n'est pas inférieur à bl (en effet la soustraction virtuelle al-bl ne provoque pas de dépassement de capacité, ce qui revient à dire que al-bl>=0 donc al>=bl qui est bien la négation de al<bl, d'une manière générale une syntaxe du type cmp x,y répond à la question de savoir si x<y, CF=1 oui, CF=0 non). Le coup pointé ne nous intéresse pas car on veut un minimum, donc si CF=0, on saute à l'adresse Meilleur20 (jnc Meilleur20). Si ce saut n'a pas lieu, on a alors CF=1, donc al est strictement inférieur à bl, cela signifie qu'on tient ici un minimum plus petit que le minimum courant que contient bl. Donc on sauvegarde dans bl ce nouveau minimum (mov bl,al) et on mémorise l'adresse de la série historique dans edi (mov edi,esi). Cela signifie que si l'analyse en restait là, on jouerait le coup pointé par edi car le nombre associé à l'offset est le minimum non nul du groupe historique. Arrivé à Meilleur20, on passe à la série historique suivante, il suffit pour cela d'ajouter 4 à esi puisque chaque série se constitue de 4 octets (add esi,4). Puis on boucle en sautant à l'adresse Meilleur10 (jmp Meilleur10) où l'on va traiter cette nouvelle série du groupe historique considéré. Arrivé à Meilleur30, on teste dh qui indique si oui ou non on a trouvé un coup dans l'historique (test dh,dh). Si ZF=1, cela signifie qu'après lecture de tout l'historique on a dh=0 c'est-à-dire qu'on n'a pas trouvé de coup, dans ce cas on saute à l'adresse Meilleur50 pour sortir immédiatement de la fonction (jz Meilleur50). Comme on sort dans ce cas avec dh=0, la boucle en while conclura qu'on n'a pas pu jouer, la boucle s'arrêtera et on reculera. Si ce saut n'a pas lieu, alors il existe un vrai coup car dh n'est pas nul, le meilleur coup est pointé par edi. Comme on va jouer ce coup, on incrémente NbCoups (inc NbCoups). Puis on va faire pointer esi dans la zone Chemin à l'endroit où l'on doit enregistrer le coup adopté à savoir à l'adresse Chemin+NbCoups*2. On commence ce petit calcul en mettant ebx à 0 (xor ebx,ebx), puis on charge dans bl le nombre de coups (mov bl,NbCoups), donc ebx sur 32 bits est égal à NbCoups. On ajoute bx à lui-même (add bx,bx) ce qui multiplie bx par 2, donc ebx=NbCoups*2. On fait pointer esi à l'adresse Chemin (lea esi,Chemin). Dans ces conditions, on doit écrire l'offset en x du coup joué à l'adresse esi+ebx et l'offset en y à l'adresse esi+ebx+1. Mais edi ne pointe que les offsets à jouer et non pas la case en coordonnées, ces coordonnées sont à calculer en fonction de la position du cavalier disponible en lecture dans la zone Chemin. On va donc lire aux adresses esi+ebx-2 et esi+ebx-1, ces deux valeurs correspondant à la position du cavalier pour l'instant, il s'agit d'ajouter à cette position (x,y) courante les offsets à jouer lesquels sont aux adresses edi+1 et edi+2 c'est-à-dire aux deuxième et troisième cases de la série historique jouée. On charge donc dans al la position en x du cavalier (mov al,[esi+ebx-2]), on charge dans dl l'offset en x de l'historique (mov dl,[edi+1]), on ajoute cet offset à al (add al,dl), al est donc maintenant égal à la nouvelle position en x du cavalier, on écrit cette valeur à l'adresse esi+ebx (mov [esi+ebx],al) ce qui correspond à l'enregistrement dans la zone Chemin. On procède au même calcul en y en chargeant dans la zone Chemin la position en y du cavalier (mov al,[esi+ebx-1]). On lit dans dl l'offset en y de la série historique (mov dl,[edi+2]). On ajoute dl à al (add al,dl), al est donc égal à la nouvelle position en y du cavalier, position que l'on enregistre dans la zone Chemin (mov [esi+ebx+1],al). Il ne reste plus qu'à effacer le coup de la série historique (mov byte ptr [edi],0), on n'efface que l'offset c'est-à-dire la première case de la série, si donc le cavalier se retrouve dans cette même configuration, un autre coup que celui-ci sera nécessairement joué si toutefois il reste un coup possible dans ce groupe historique. La fonction est terminée, on rend la main à l'appelant (ret).

Sélectionnez
Meilleur:    xor  dh,dh                // flag de coup trouvé
             mov  esi,pHisto           // esi pointe l&#146;historique
             mov  bl,255               // init du minimum au maxi
Meilleur10:  mov  al,[esi]             // coup historisé
             inc  al                   // fin?
             jz   Meilleur30           // oui saute, fin d&#146;historique
             dec  al                   // coup effacé?
             jz   Meilleur20           // oui saute
             cmp  NbCoups,62           // dernier coup?
             jnz  Meilleur15           // non cas général
             mov  edi,esi              // oui adresse du coup unique
             jmp  Meilleur40           // saute pour l'enregistrer
Meilleur15:  mov  al,[esi+3]           // nb de coups possibles
             test al,al                // cul de sac?
             jz   Meilleur20           // oui ignore
             mov  dh,1                 // non on a trouvé un vrai coup
             cmp  al,bl                // minimum plus petit encore?
             jnc  Meilleur20           // non continue la recherche
             mov  bl,al                // oui nouveau minimum
             mov  edi,esi              // mémorise l'adresse du minimum
Meilleur20:  add  esi,4                // série historique suivante
             jmp  Meilleur10           // continue jusqu'à la fin
Meilleur30:  test dh,dh                // rien?
             jz   Meilleur50           // oui retour avec dh=0 (rien)
Meilleur40:  inc  NbCoups              // un coup de plus
             xor  ebx,ebx              // ebx=0
             mov  bl,NbCoups           // ebx=NbCOups
             add  bx,bx                // ebx*2 donc ebx=NbCoups*2
             lea  esi,Chemin           // on va enregistrer à esi+ebx
             mov  al,[esi+ebx-2]       // al=x d'où l'on vient
             mov  dl,[edi+1]           // dl=offset en x choisi
             add  al,dl                // al=x où l'on va
             mov  [esi+ebx],al         // enregistré dans Chemin
             mov  al,[esi+ebx-1]       // al=y d'où l'on vient
             mov  dl,[edi+2]           // dl=offset en y choisi
             add  al,dl                // al=y où l'on va
             mov  [esi+ebx+1],al       // enregistré dans Chemin
             mov  byte ptr [edi],0     // coup effacé de l'historique
Meilleur50:  ret                       // retour à l&#146;appelant

Dans ces conditions le sous-programme Joue est très court. On commence par rechercher et enregistrer dans le bon groupe historique tous les coups possibles (call Cherche). Cette fonction, comme nous l'avons vu, renvoie dh. Bien que dh soit un registre 8 bits, on le considère ici comme un flag. Si dh<>0, alors il y a au moins un coup d'enregistré dans le groupe historique concerné. Si dh=0, rien n'a été trouvé donc rien n'a été enregistré dans l'historique, on est dans un cul-de-sac, il faudra reculer. On teste donc la valeur de dh pour savoir si dh est nul ou non (test dh,dh). Si ZF=1, alors dh=0 et donc on n'a rien trouvé, on saute à l'adresse Joue10 pour retourner à l'appelant (jz Joue10). Si ce saut n'a pas lieu, il existe alors au moins un coup dans le groupe historique, on cherche et on joue le meilleur de ces coups historisés (call Meilleur), il s'agit, comme nous l'avons dit, du coup associé à un nombre de possibilités futures minimum et non nul (quatrième octet de chaque série historique). À noter qu'après avoir reculé et qu'après avoir testé pour un groupe historique précédent la présence d'un coup possible (coup donc non encore joué), le C++ nous branche à cette adresse à savoir Joue5. En effet, il faut surtout ne pas recalculer via Cherche tous les coups possibles et donc ne pas se brancher à Joue sinon le programme bouclerait et trouverait sans cesse les mêmes solutions. Il faut se brancher à Joue5 de manière à choisir un autre coup présent dans l'historique parmi ceux qui n'ont pas encore été effacés. C'est ce procédé qui nous assure de trouver chaque fois un chemin différent et qui épuise toutes les solutions possibles. Meilleur renvoie dh pour indiquer si on est autorisé à avancer encore. Si dh<>0 (true), on a joué un coup et on est autorisé à essayer d'avancer encore. Si dh=0, on est dans un cul-de-sac, soit un cul-de-sac en plein milieu de recherche, soit un cul-de-sac qui est une solution. Dans ce cas, la boucle d'avance s'arrêtera dans le programme maître. Le sous-programme Joue est terminé, on retourne à l'appelant (ret).

Sélectionnez
Joue:        call Cherche              // recherche tous les coups
             test dh,dh                // cul de sac ou solution?
             jz   Joue10               // oui cassos avec dh=0
Joue5:       call Meilleur             // non choisis le meilleur coup
Joue10:      ret                       // retour avec dh

Il ne nous reste qu'un seul petit sous-programme à écrire, c'est Ecritdh qui écrit le contenu du registre dh dans la case visée par le cavalier. Si dh=1, cela signifiera que le cavalier passe par cette case, si dh=0 cela signifiera qu'on libère la case mais le sous-programme ne s'occupe pas de la valeur de dh, il se contente de calculer le bon offset et d'y écrire dh. De quelle case s'agit-il ? Il s'agit de la case située à la fin de la zone Chemin, case à laquelle on accède (cas où dh=1) ou qu'on quitte (cas où dh=0). Comme il y a NbCoups joués, l'adresse de lecture dans Chemin est Chemin+NbCoups*2. À cette adresse on lira l'offset en x et à l'adresse suivante l'offset en y. Ces deux offsets compris entre 0 et 7 ayant été lus, on calcule 10*y+x, ce nombre correspond à ce qu'il faut ajouter à Echiquier+21 (case A1) pour pointer la bonne case. Une fois pointée, on y écrit dh. On commence donc par pointer au début de la zone Chemin (lea esi,Chemin). On va calculer dans ebx la valeur NbCoups*2, on commence par annuler ebx de manière à ce que les 24 bits de poids fort soient nuls (xor ebx,ebx), on charge NbCoups dans bl (mov bl,NbCoups). Ainsi bl=NbCoups mais comme les 24 bits de poids fort sont nuls, on a aussi ebx=NbCoups. Pourquoi devons-nous procéder ainsi ? Simplement parce qu'une syntaxe du type esi+bl n'existe pas, esi est sur 32 bits, il faut que les opérandes soient de même format, donc la syntaxe est esi+ebx, c'est pourquoi pour lire une valeur 8 bits, on commence toujours par mettre le registre 32 bits à 0, on lit les 8 bits, tout le registre contient alors sur 32 bits la valeur 8 bits, ce qui nous permet d'utiliser la syntaxe esi+ebx. Donc ici on a ebx=NbCoups. On multiplie par deux (add bx,bx), donc ebx=NbCoups*2. On lit dans la zone Chemin l'octet d'adresse esi+ebx+1 (mov al,[esi+ebx+1]), al est alors égal à la position en y du cavalier. On va multiplier par 10 cette valeur. Pour ce faire, on écrit 10 dans le registre 8 bits cl (mov cl,10) puis on multiplie al par cl (mul cl), le résultat se trouve dans ax sur 16 bits. Cela dit, comme le chiffre maximal est 7, le résultat maximal sera 70, cela tient sur 8 bits, donc al contient seul cette valeur et ah=0. Ceci nous permet d'utiliser ah sans perte d'information, ce que nous faisons puisque nous lisons dans ah la position en x du cavalier (mov ah,[esi+ebx]). On ajoute maintenant ah à al (add al,ah), donc al=10*y+x, la valeur maximale est 77 (7*10+7). On écrit ce résultat dans bl (mov bl,al). Comme ebx a été remis à 0, ebx=10*y+x à savoir l'offset qu'il faut ajouter à Echiquier+21 (case A1) pour pointer la bonne case de l'échiquier. Donc on fait pointer esi à la case A1 (lea esi,Echiquier+21), la case correspondante est donc esi+ebx, on y écrit dh (mov [esi+ebx],dh). Nous ne savons pas à quoi est égal dh et cela ne nous importe peu. On sait seulement qu'on appelle Ecritdh avec dh=0 ou dh=1 et que si dh=1 cela interdira l'utilisation future de cette case car le cavalier ne peut visiter que les cases libres et si dh=0 cela libérera la case pour une possibilité d'utilisation future. C'est fini, dh a été écrit sur la bonne case de notre échiquier, on retourne à l'appelant (ret).

Sélectionnez
Ecritdh:     lea  esi,Chemin
             xor  ebx,ebx
             mov  bl,NbCoups           // ebx nb de coups déjà joués
             add  bx,bx                // ebx*2
             mov  al,[esi+ebx+1]       // al = offset en y
             mov  cl,10
             mul  cl                   // al=10y
             mov  ah,[esi+ebx]         // ah = x
             add  al,ah                // al=10y+x
             mov  bl,al                // ebx=10y+x
             lea  esi,Echiquier+21     // esi sur A1
             add  esi,ebx              // + offset=case où l'on est
             mov  [esi],dh             // écrit dh dans la case
             ret

Le reste du programme concerne le graphique et est écrit en C++. La fonction Dessine dessine dans le bitmap la position correspondant à la zone Chemin, ce dessin se créant à partir d'un bitmap vide. Pour chaque couple (x,y) de la zone Chemin, on calcule les coordonnées graphiques correspondantes et on y affiche le numéro du coup associé. Ainsi, chaque fois qu'un coup est ajouté dans Chemin, un numéro supplémentaire apparaît dans une case, ce qui simule l'avancée du cavalier. Après un recul, NbCoups a été décrémenté, un couple (x,y) de moins sera visité, la case ignorée à la fin de la zone Chemin reste alors vide (car on redessine à partir d'un bitmap effacé) ce qui donne l'impression que le cavalier recule. On commence par effacer le bitmap :

Sélectionnez
BM->Canvas->Brush->Style=bsSolid;
BM->Canvas->FillRect(Rect(0,0,BM->Width,BM->Height));

La précision bsSolid est nécessaire, tout le bitmap se remplit avec la couleur BM->Canvas->Brush->Color, cette couleur est initialisée à blanc au moment de la création du bitmap par new (blanc est la couleur par défaut de Brush), on la laisse donc telle quelle. Nous allons maintenant dessiner les lignes correspondant à l'échiquier, on va les dessiner avec en bleu clair :

Sélectionnez
BM->Canvas->Pen->Color=clAqua;

On commence par les 9 traits horizontaux de l'échiquier. On initialise une variable Offset avec CoinTop. À chaque ligne, on pose le crayon sur le point (CoinLeft,Offset) puis on trace un trait horizontal jusqu'à CoinLeft+LarCase*8, Offset). Comme chaque case à une largeur de LarCase pixels, LarCase*8 correspond à la longueur de l'échiquier. Puis on ajoute LarCase à Offset pour la prochaine ligne horizontale.

Sélectionnez
Offset=CoinTop;
for(i=0;i<9;i++)
  {
  BM->Canvas->MoveTo(CoinLeft,Offset);
  BM->Canvas->LineTo(CoinLeft+LarCase*8,Offset);
  Offset+=LarCase;
  }

Puis on trace les 9 lignes verticales, on obtient ainsi une grille de 8x8. Le principe est le même que précédemment mais la variable Offset va évoluer en x et non en y.

Sélectionnez
Offset=CoinLeft;
for(i=0;i<9;i++)
  {
  BM->Canvas->MoveTo(Offset,CoinTop);
  BM->Canvas->LineTo(Offset,CoinTop+LarCase*8);
  Offset+=LarCase;
  }

Ensuite, nous allons écrire les lettres et les chiffres au bord de l'échiquier, nous allons les écrire en noir.

Sélectionnez
BM->Canvas->Font->Color=clBlack;

On commence par écrire les lettres allant de A à H horizontalement en bas de l'échiquier. On prend comme coordonnées en y CoinTop+LarCase*8+10, on se situe donc à 10 pixels en dessous de la première ligne horizontale en partant du bas. On initialise la variable Offset à CoinLeft+Larcase/3 ce qui nous positionne au premier tiers de la case. On entre dans une boucle en i, pour chaque occurrence on écrit le caractère correspondant au code ascii 65+i, on affichera donc A pour i=0, B pour i=1 etc. Après chaque affichage, on avance le pointeur graphique en x c'est-à-dire qu'on ajoute LarCase à Offset.

Sélectionnez
Offset=CoinLeft+LarCase/3;
Ligne=CoinTop+LarCase*8+10;
for(i=0;i<8;i++)
  {
  BM->Canvas->TextOutA(Offset,Ligne,char(65+i));
  Offset+=LarCase;
  }

Principe similaire pour écrire les huit chiffres verticalement. On initialise la variable Offset avec CoinTop+Larcase*22/3. Comme 22/3 est égal à 7+1/3, on pointe le premier tiers en y, là où l'on va écrire le 1. Pour les x, on choisit CointLeft-15, on se trouve alors à 15 pixels de la première ligne verticale. À chaque occurrence de la boucle en i, on affiche le code ascii 49+i, donc 1 pour i=0, 2 pour i=1 etc. Puis on soustrait LarCase à notre Offset, on remonte ainsi notre coordonnée en y, là où doit s'écrire le chiffre suivant.

Sélectionnez
Offset=CoinTop+LarCase*22/3;
Ligne=CoinLeft-15;
for(i=0;i<8;i++)
  {
  BM->Canvas->TextOutA(Ligne,Offset,char(49+i));
  Offset-=LarCase;
  }

Nous allons maintenant visiter la zone Chemin et écrire dans les cases de l'échiquier le numéro des coups, nous allons écrire ces numéros en rouge.

Sélectionnez
BM->Canvas->Font->Color=clRed;

Pour chaque couple (x,y) de la zone Chemin, on lit x puis y. L'offset d'affichage en x sera CoinLeft+x*LarCase+10, on se trouve à 10 pixels à l'intérieur de la case. L'offset en y sera CoinTop+(8-y)*LarCase-20 soit à 20 pixels à l'intérieur de la case. Ces coordonnées étant calculées, on affiche le numéro du coup soit i+1 dans la boucle en i.

Sélectionnez
for(i=0;i<NbCoups+1;i++)
  {
  x=Chemin[i*2];
  y=Chemin[i*2+1];
  Offset=CoinLeft+x*LarCase+10;
  Ligne=CoinTop+(8-y)*LarCase-20;
  BM->Canvas->TextOutA(Offset,Ligne,IntToStr(i+1));
  }

À ce stade, la totalité du bitmap a été dessiné hors écran, il faut maintenant forcer un paint. La fonction Dessine reçoit à cet effet le Sender (TObject*) nécessaire à la fonction OnPaint. Il suffit, pour forcer un paint d'appeler la méthode OnPaint de Form1, donc :

Sélectionnez
Form1->OnPaint(S);

où S est le Sender reçu en argument dans la fonction. Cet OnPaint n'est appelé qu'une seule fois directement par notre programme car il faut mettre à jour l'écran après que le bitmap a été redessiné. Tous les autres OnPaint seront appelés par le système en cas de nécessité sans que nous nous en occupions. À chaque fois, le processus sera le même, recopier via draw le bitmap dans le canvas de Form1.

Sélectionnez
void __fastcall TForm1::FormPaint(TObject *Sender)
{
Form1->Canvas->Draw(0,0,BM);
}

Le seul objet qui a été instancié par new est notre bitmap, il convient de restituer la mémoire allouée au moment du OnDestroy de Form1.

Sélectionnez
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
delete BM;
}

II. Programme complet▲

Pour faire fonctionner immédiatement ce programme :

1) Entrez dans C++ Builder ;
2) Faites Fichier|Enregistrer le projet sous ;
3) À la place de unit1 choisissez cavalier ;
4) À la place de Project1 choisissez Hamilton ;
5) Double-cliquez sur les événements OnActivate, OnDestroy et OnPaint
de Form1 dans l'inspecteur d'objets ;

Cette petite manipulation nous a créé les trois méthodes FormActivate , FormDestroy et FormPaint dans la classe TForm1 maintenant disponibles dans l'en-tête cavalier.h :

Sélectionnez
class TForm1 : public TForm
{
__published:        // Composants gérés par l'EDI
                    void __fastcall FormActivate(TObject *Sender);
                    void __fastcall FormDestroy(TObject *Sender);
                    void __fastcall FormPaint(TObject *Sender);
private:            // Déclarations utilisateur
public:             // Déclarations utilisateur
                  __fastcall TForm1(TComponent* Owner);
};

6) Effacer tout ce qui se trouve dans cavalier.cpp créé par C++ Builder ;
7) Copiez-collez dans cavalier.cpp maintenant vide le source ci-dessous ;
8) Sauvagardez tout (cliquez sur les disquettes en cascade) ;
9) Faites F9 pour compiler et exécuter le programme.

le nom proposé cavalier à la place de unit1 est important à cause de l'include cavalier.h dans le cpp.

en créant les trois méthodes d'événements, vous créez aussi leur prototype en temps réel dans le cpp mais cela n'a aucune importance puisque l'étape 6 efface précisément le contenu de ce cpp pour le remplacer par le source ci-dessous. Ce qui importe, c'est que ces trois méthodes aient été déclarées dans cavalier.h et que ces liens existent dans l'inspecteur d'objets.

vous ne pouvez faire fonctionner ce tutoriel que si vous avez l'assembleur c'est-à-dire l'exécutable tasm32.exe dans votre répertoire bin. Quand bcc32.exe (le compilateur C++) rencontre la directive asm, il appelle l'assembleur à savoir tasm32.exe. Au début du source ci-dessous, il y a la directive #pragma inline, c'est cette directive qui indique au C++ (bcc32) qu'il va y avoir de l'assembleur (tasm32) par la suite.

Sélectionnez
//---------------------------------------------------------------------------
//-------------- H A M I L T O N   D E B U T --------------------------------
//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#pragma inline
#include "cavalier.h"
#pragma package(smart_init)
#pragma resource "*.dfm"

//---------------------------------------------------------------------------
/* Les messages */
const char Msg[][100]=
{"Case de départ", //0
"Donnez la case de départ (par exemple E4)" //1
};

/* Coordonnées d'affichage
largeur des cases de l'échiquier
longueur d'une ligne d'historique soit
7 fois "off, x, y, n" et code fin = 33 octets
Solutions = nb de solutions à afficher */
const int CoinLeft  = 50,
          CoinTop   = 30,
          LarCase   = 30,
          LigHisto  = 33,
          Solutions = 10;

//---------------------------------------------------------------------------
/* Pointeur de la fenêtre principale */
TForm1 *Form1;

/* Pointeur pour un bitmap hors écran */
Graphics::TBitmap *BM;

/* Chemin emprunté par le cavalier, série d'offsets x y
Zone déclarée en C++ car utilisée par le C++ pour afficher
le numéro des coups dans les cases par une syntaxes du type Chemin[i] */
char Chemin[128];

/* Case de départ + zéro de fin de chaîne */
char Depart[3];

/* Nombre de coups joués */
char NbCoups;

/* Nb de solutions trouvées */
int NbSol;

/* flags d'avance et de recul autorisés */
bool AvanceOK, ReculeOK;

/* pointeur dans l'historique */
char *pHisto;

//---------------------------------------------------------------------------
asm
{
/* Échiquier entouré de codes 255 hors écran (A1=Échiquier+21) */
Echiquier db 255,255,255,255,255,255,255,255,255,255
          db 255,255,255,255,255,255,255,255,255,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255, 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,255
          db 255,255,255,255,255,255,255,255,255,255
          db 255,255,255,255,255,255,255,255,255,255

/* Les huit directions du cavalier en termes d'offsets d'adresse et (x,y) */
DirC dd      8,  -2,  1
     dd     12,   2,  1
     dd     19,  -1,  2
     dd     21,   1,  2
     dd     -8,   2, -1
     dd    -12,  -2, -1
     dd    -19,   1, -2
     dd    -21,  -1, -2

/* Historique
Chaque groupe de LigHisto octets se constitue de 8 coups maxi de 4 octets
et d'un code fin (255) d'où LigHisto=33 (ligne d'historique)
case possible (offset d'adresse), Offx, Offy,
nb de coups qui seraient acceptés pour cette case */
Histo db LigHisto*63 dup(?)
}

//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
                    : TForm(Owner)
{
/* ni coup ni solution */
NbCoups=0;
NbSol=0;

/* Création du bitmap aux dimensions de l'écran (surdimension) en
quatre bits par pixel */
BM = new Graphics::TBitmap();
BM->PixelFormat=pf4bit;
BM->Width=Screen->Width;
BM->Height=Screen->Height;

/* Quelques propriétés de la fenêtre principale (application) */
Form1->Width=380;
Form1->Height=380;
Form1->Top=90;
Form1->Left=30;
}

//---------------------------------------------------------------------------
asm
{
/* calcul des offsets x et y de la case de départ
par rapport à la case A1 située à l'adresse Echiquier+21
et écriture dans le premier couple (x,y) de Chemin de cette case */

CalOCD:      mov al,Depart             // case de départ (lettre)
             sub al,65                 // entre 0 et 7
             mov Chemin,al             // enregistré
             mov al,Depart+1           // case de départ (chiffre)
             sub al,49                 // entre 0 et 7
             mov Chemin+1,al           // enregistré
             ret                       // fin

/* Cherche tous les coups et joue le meilleur s'il y a au moins un coup
renvoie al qui indique si un coup a été joué ou non
si oui (al<>0 true), la boucle en while du C++ continuera
si non (al=0 false), la boucle en while s'arrêtera */

Joue:        call Cherche              // recherche tous les coups possibles
             test dh,dh                // cul de sac?
             jz   Joue10               // oui cassos avec dh=0
Joue5:       call Meilleur             // non choisis le meilleur coup historisé
Joue10:      ret                       // dh flag d'avance réalisée

//--------------
/* Cherche tous les coups possibles et mémorise dans l'historique
pour chacun d'eux off, x, y ainsi que le nombre de coups dont
nous disposerions si le coup associé était joué.
Renvoie dh indiquant s'il y a au moins un coup ou non, dh=1 oui, dh=0 non.

On commence par faire pointer pHisto sur le bon groupe historique
c'est-à-dire à l'adresse Histo+NbCoups*LigHisto et on écrit
le code fin à cette adresse */

Cherche:     lea  esi,Histo
             xor  eax,eax
             mov  al,NbCoups
             mov  cl,LigHisto
             mul  cl                   // eax=LigHisto*NbCoups
             add  esi,eax              // esi pointe le bon groupe historique
             mov  pHisto,esi           // mémorisé dans pHisto
             mov  byte ptr [esi],255   // code fin au début de ce groupe

             mov  dh,1                 // on va écrire 1 dans la case
             call Ecritdh              // la case est occupée

             xor  dh,dh                // dh=0 flag de coup trouvé
             lea  edi,DirC             // table des directions
             mov  cl,8                 // 8 directions possibles
Cherche10:   mov  ebx,[edi]            // ebx = une des directions
             mov  al,[esi+ebx]         // al=contenu de cette case
             test al,al                // case vide?
             jnz  Cherche20            // non on passe à la suivante
             call Combien              // oui calcule le nb de coups possibles
Cherche20:   add  edi,12               // direction suivante
             dec  cl                   // fin?
             jnz  Cherche10            // non refelemele
             ret                       // renvoie dh flag de coup trouvé

//--------------
/* Recherche combien de coups nous aurions à notre disposition
si on choisissait cette case. L'offset testé est dans ebx sur 32 bits
ou bl sur 8 bits (ebx=bl)
On en profite pour mettre dh à 1 puisque si on est arrivé là, c'est
qu'on a trouvé au moins une destination possible */

Combien:     push esi
             push ecx
             push edi
             mov  dh,1                 // on a trouvé un coup
             add  esi,ebx              // esi sur cette case
             lea  edi,DirC             // table des directions
             mov  cl,8                 // 8 directions
             xor  ch,ch                // ch=0 compteur de coups acceptés
Combien10:   mov  ebx,[edi]            // ebx = une des directions
             mov  al,[esi+ebx]         // al=contenu de cette case
             test al,al                // case libre?
             jnz  Combien20            // non saute
             inc  ch                   // oui compteur+1 de coups possibles
Combien20:   add  edi,12               // direction suivante
             dec  cl                   // fin?
             jnz  Combien10            // non refelemele

/* ici ch est égal au nombre de coups possibles pour l'offset testé,
on mémorise ces données dans l'historique. On recherche
d'abord le code fin 255 dans le groupe historique puis on écrit les données
et le code fin */

             mov  esi,pHisto           // pHisto pointe déjà le bon groupe
Combien30:   mov  al,[esi]             // direction historique
             add  esi,4                // avance toujours
             inc  al                   // code fin?
             jnz  Combien30            // non continue jusqu'au code fin

/* ici esi pointe dans l'historique la série d'après le code fin,
on écrase ce code fin en faveur de l'offset à mémoriser, on écrit x,y et
le nb de coups jouables et on réecit à la suite la code fin */

             mov  [esi-1],ch           // nombre de coups jouables
             mov  byte ptr [esi],255   // code fin pour la série suivante
             pop  edi                  // récupère la direction testée
             mov  al,[edi]             // al=direction testée (LSB)
             mov  [esi-4],al           // dans l'historique
             mov  al,[edi+4]           // al= offset en x
             mov  [esi-3],al           // dans l'historique
             mov  al,[edi+8]           // al= offset en y
             mov  [esi-2],al           // dans l'historique
             pop  ecx
             pop  esi
             ret

//--------------
/* Recherche du meilleur coup parmi les coups historisés
renvoie edi pointant ce meilleur coup dans le bon groupe historique
renvoie aussi dh à l'appelant pour indiquer si un coup a été
trouvé ou non, dh=1 oui, dh=0 non */

Meilleur:    xor  dh,dh                // flag de coup trouvé
             mov  esi,pHisto           // esi pointe le bon groupe historique
             mov  bl,255               // init du minimum au maxi
Meilleur10:  mov  al,[esi]             // coup historisé
             inc  al                   // fin?
             jz   Meilleur30           // oui saute pour essayer d'enregistrer
             dec  al                   // coup effacé?
             jz   Meilleur20           // oui saute

/* ici un coup a été trouvé. On traite le cas particulier du dernier
coup pour lequel on accepte un cul-de-sac. D'ailleurs c'est forcément
un cul-de-sac, si NbCoups=62 (car NbCoups n'a pas encore été incrémenté,
63 étant le nombre de coups total du cavalier), cela suffit pour accepter
le coup historique trouvé d'où saut à Meilleur30 */

             cmp  NbCoups,62           // dernier coup?
             jnz  Meilleur15           // non cas général
             mov  edi,esi              // oui adresse du coup unique
             jmp  Meilleur40           // saute pour l'enregistrer

/* Ici cas général, on ne tolère pas le cul-de-sac */

Meilleur15:  mov  al,[esi+3]           // nb de coups possibles
             test al,al                // cul de sac?
             jz   Meilleur20           // oui ignore cette série historique

/* ici on est certain de trouver un coup historisé donc dh=1 */

             mov  dh,1                 // non on a trouvé un vrai coup
             cmp  al,bl                // minimum plus petit encore?
             jnc  Meilleur20           // non continue la recherche
             mov  bl,al                // oui nouveau minimum
             mov  edi,esi              // mémorise l'adresse du minimum
Meilleur20:  add  esi,4                // série historique suivante
             jmp  Meilleur10           // continue jusqu'à la fin de l'historique

/* ici l'historique a été lu entièrement pour le coup en cours,
reste à savoir si on a trouvé un coup */

Meilleur30:  test dh,dh                // rien?
             jz   Meilleur50           // oui retour immédiat avec dh=0 (rien)

/* enregistre le meilleur coup pointé par edi dans Chemin.
On fait pointer esi à Chemin+NbCoups*2 (car il y a deux octets par coup)
puis on écrit x à esi et y à esi+1 . Comme edi pointe le coup historique
choisi, on ajoute ces directions mémorisées dans cette série
historique à la position du cavalier de manière
à ce que Chemin contienne les coordonnées réelles successives.
On termine en effaçant ce coup de l'historique pour qu'il ne soit pas rejoué */

Meilleur40:  inc  NbCoups              // un coup de plus
             xor  ebx,ebx
             mov  bl,NbCoups
             add  bx,bx                // ebx*2
             lea  esi,Chemin
             mov  al,[esi+ebx-2]       // al=x d'où l'on vient
             mov  dl,[edi+1]           // dl=offset en x choisi
             add  al,dl                // al=x où l'on va
             mov  [esi+ebx],al         // enregistré
             mov  al,[esi+ebx-1]       // al=y d'où l'on vient
             mov  dl,[edi+2]           // dl=offset en y choisi
             add  al,dl                // al=y où l'on va
             mov  [esi+ebx+1],al       // enregistré
             mov  byte ptr [edi],0     // coup effacé de l'historique
Meilleur50:  ret                       // retour avec dh et edi si dh=1

//--------------
/* revoie al flag de continuation de recul
si ce S/P renvoie al=0 (false) on arrêtera la boucle en while dans le C++
c'est-à-dire qu'on arrêtera de reculer car un coup a été trouvé dans
l'historique.
Si al<>0 (true), aucun coup n'a été trouvé dans l'historique donc
on continuera la boucle en while pour reculer à nouveau */

Recule:      xor  dh,dh                // dh=0
             call Ecritdh              // libère la case de recul
             dec  NbCoups              // un coup de moins

/* ici on calcule dans esi l'adresse du bon groupe historique
qui est égale à Histo+NbCoups*LigHisto (puisqu'il y a LigHisto
octets historiques par coup). Ce groupe historique correspond
au groupe précédent puisqu'on vient de décrémenter NbCoups */

             lea  esi,Histo
             xor  eax,eax
             mov  al,NbCoups
             mov  cl,LigHisto
             mul  cl
             add  esi,eax              // esi sur le groupe histo
             mov  pHisto,esi           // mémorisation du pointeur historique

Recule10:    mov  al,[esi]             // offset historique
             cmp  al,255               // fin du groupe historique?
             jz   Recule40             // oui saute avec al=255<>0
             test al,al                // coup effacé?
             jnz  Recule20             // non saute
Recule15:    add  esi,4                // oui groupe suivant
             jmp  Recule10             // continue la recherche
Recule20:    mov  al,[esi+3]           // nb de coups possibles
             test al,al                // cul-de-sac?
             jz   Recule15             // oui continue de chercher
             xor  al,al                // non on a un coup, retour avc al=0
Recule40:    ret                       // al = flag de continuation de recul

//--------------
/* Écrit dh dans la case correspondant au coup enregistré dans Chemin
dh est est égal à 1 pour occuper la case
dh est est égal à 0 pour libérer la case */

Ecritdh:     lea  esi,Chemin
             xor  ebx,ebx
             mov  bl,NbCoups           // ebx nb de coups déjà joués
             add  bx,bx                // ebx*2
             mov  al,[esi+ebx+1]       // al = offset en y
             mov  cl,10
             mul  cl                   // al=10y
             mov  ah,[esi+ebx]         // ah = x
             add  al,ah                // al=10y+x
             mov  bl,al                // ebx=10y+x
             lea  esi,Echiquier+21     // esi sur A1
             add  esi,ebx              // + offset=case où l'on est
             mov  [esi],dh             // écrit dh dans la case
             ret
}

//---------------------------------------------------------------------------
void Dessine(TObject *S)
{
int i, Offset, Ligne;
char x,y;

/* Efface tout le bitmap, on redessine à partir de rien. La couleur
par défaut pour le remplissage est le blanc, donc on laisse tel quel
sinon il faudrait initialiser BM->Canvas->Brush->Color à la couleur voulue */
BM->Canvas->Brush->Style=bsSolid;
BM->Canvas->FillRect(Rect(0,0,BM->Width,BM->Height));

/* Couleur des traits dessinant l'échiquier */
BM->Canvas->Pen->Color=clAqua;

/* Traits horizontaux de l'échiquier */
Offset=CoinTop;

for(i=0;i<9;i++)
  {
  BM->Canvas->MoveTo(CoinLeft,Offset);
  BM->Canvas->LineTo(CoinLeft+LarCase*8,Offset);
  Offset+=LarCase;
  }

/* Traits verticaux de l'échiquier */
Offset=CoinLeft;

for(i=0;i<9;i++)
  {
  BM->Canvas->MoveTo(Offset,CoinTop);
  BM->Canvas->LineTo(Offset,CoinTop+LarCase*8);
  Offset+=LarCase;
  }

/* Coordonnées en noir */
BM->Canvas->Font->Color=clBlack;

/* On écrit les lettres de A à H */
Offset=CoinLeft+LarCase/3;
Ligne=CoinTop+LarCase*8+10;

for(i=0;i<8;i++)
  {
  BM->Canvas->TextOutA(Offset,Ligne,char(65+i));
  Offset+=LarCase;
  }

/* On écrit les chiffres de 1 à 8 */
Offset=CoinTop+LarCase*22/3;
Ligne=CoinLeft-15;

for(i=0;i<8;i++)
  {
  BM->Canvas->TextOutA(Ligne,Offset,char(49+i));
  Offset-=LarCase;
  }

/* Affiche le numéro des coups dans les cases en rouge */
BM->Canvas->Font->Color=clRed;
for(i=0;i<NbCoups+1;i++)
  {
  x=Chemin[i*2];
  y=Chemin[i*2+1];
  Offset=CoinLeft+x*LarCase+10;
  Ligne=CoinTop+(8-y)*LarCase-20;
  BM->Canvas->TextOutA(Offset,Ligne,IntToStr(i+1));
  }

/* appel du OnPaint de Form1 */
Form1->OnPaint(S);
}

//---------------------------------------------------------------------------
void __fastcall EN_AVANT(TObject* S)
{
  /* Boucle générale d'avance
  on avance jusqu'à cul-de-sac */
  AvanceOK=true;
  while(AvanceOK)
     {
     asm
     {
        call Joue            // joue le meilleur coup s'il y a un coup
        mov  AvanceOK,dh     // témoin de coup joué
     }

  /* Affiche le nouveau coup joué */
  Dessine(S);

  } // fin du while(AvanceOK)

  /* Si le cavalier a joué 63 fois, c'est donc qu'on a trouvé une solution */
  if(NbCoups==63)
    {
    AnsiString A;
    A="Solution numéro "+IntToStr(++NbSol);
    Application->MessageBox(A.c_str(),"Solution",MB_OK);
    }

   /* On n'acceptera de reculer que si l'on n'est pas arrivé
   au nombre de solutions requis. Cette petite précaution
   nous permettra de laisser affichée
   une solution complète à l'écran à la fin du processus
   puisque alors le recul n'aura pas lieu */
   ReculeOK=(NbSol!=Solutions);
}
//---------------------------------------------------------------------------
void __fastcall EN_ARRIERE(TObject* S)
{
/* Sortie immédiate si recul refusé.
Ce refus n'arrive qu'à la fin de l'exécution quand on a affiché le nombre
correct de solutions (voir constante Solutions en en-tête). Ainsi, quand
le programme rendra la main à l'utilisateur, une solution entière restera
affichée du fait que ce recul est refusé. Si on ne prenait pas cette
petite précaution, le dernier affichage serait incomplet puisqu'un recul
de quelques coups aurait été exécuté. */
if(!ReculeOK) return;

/* Boucle générale de recul */
while(ReculeOK)
  {
  asm
    {
         call Recule          // recule
         mov  ReculeOK,al     // témoin de recul à continuer
    }

    /* Efface la case vide libérée */
    Dessine(S);

  } // fin du while ReculeOK

/* Ici on peut réavancer, on cherche un coup dans l'historique.
Il sera forcément différent du choix précédent au même stade
puisque les coups effectivement joués sont effacés de l'historique */
asm
  {
        call Joue5
  }

/* Affiche le nouveau coup joué */
Dessine(S);
}

//---------------------------------------------------------------------------
void __fastcall InitCaval(AnsiString D, TObject* S)
{
/* recopie dans Départ de la case choisie par l'utilisateur */
strcpy(Depart,D.c_str());

/* Initialisation du calcul */
asm
{
        /* Offset de la case de départ  */
        call CalOCD
}

/* Dessine dans le bitmap, on a déjà la case de départ */
Dessine(S);
}
//---------------------------------------------------------------------------
void __fastcall Cavalier(AnsiString D, TObject* S)
{
/* Initialisations */
InitCaval(D,S);

/* Boucle générale d'avancée et de recul */
do
  {
  EN_AVANT(S);
  EN_ARRIERE(S);
  }
while(NbSol!=Solutions);
};
//---------------------------------------------------------------------------
void __fastcall TForm1::FormActivate(TObject *Sender)
{
/* CasDep case de départ choisi par l'utilisateur */
AnsiString CasDep;
int Rep;

/* Saisie de la case de départ, au sortir de la boucle en while
Rep=1, rien n'est saisi on sort de l'application
Rep=2, une case correcte est entrée, on exécute le calcul */
Rep=0;
while(!Rep)
{
  /* entrée de la case de départ */
  CasDep = InputBox(Msg[0],Msg[1],"");

  /* si rien, Rep=1 */
  if (!CasDep.Length()) Rep=1;

  /* si quelque chose, test de la validité de la saisie */
  if (  CasDep.Length()==2
     && CasDep[1]>='A'
     && CasDep[1]<='H'
     && CasDep[2]>='1'
     && CasDep[2]<='8') Rep=2;
}

/* Fin de l'application si case de départ non saisie
sinon recherche des solutions */
if(Rep==1) Close(); else Cavalier(CasDep,Sender);
}

//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
{
/* Recopie le bitmap dessiné dans le canvas de Form1 */
Form1->Canvas->Draw(0,0,BM);
}

//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
/* Restitue la mémoire utilisée pour le bitmap */
delete BM;
}
//---------------------------------------------------------------------------
//-------------- H A M I L T O N   F I N ------------------------------------
//---------------------------------------------------------------------------

Vous trouverez d'autres éléments sur le C++ et l'assembleur ici.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Gilles Louise. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Responsable de la rubrique C : le Service Publications -

Developpez.com

Nous contacter

Participez

Informations légales

Services

Forum C

Blogs

Hébergement

Partenaires

PlanetHoster
Ikoula

Copyright © 2000-2014 - www.developpez.com


D:\Com_CHM\Documentation CHM\_ASM\ASM Tutoriel\Res1\ASMTTRL.rtf

Assembler Tutorial en Mode Réel

1996 Edition
University of Guadalajara
Information Systems General Coordination.
Culture and Entertainment Web

Une convertion d'un PDF by JMM

June 12th 1995 /font>
Copyright(C)1995-1996

This is an introduction for people who want to programming in assembler language. ( Mode réel )

Copyright (C) 1995-1996, Hugo Perez. Anyone may reproduce this document, in whole or in part, provided that: (1) any copy or republication of the entire document must show University of Guadalajara as the source, and must include this notice; and (2) any other use of this material must reference this manual and , and the fact that the material is copyright by Hugo Perez and is used by permission.

Table of Contents

1. Introduction
2. Basic Concepts
3. Assembler programming
4. Assembler language instructions
5. Interruptions and file managing
6. Macros and procedures

Program examples

1. Introduction
Table of contents

1.1 What's new in the Assembler material
1.2 Presentation
1.3 Why learn Assembler language
1.4 We need your opinion

1.1 What's new in the Assembler material

After of one year that we've released the first Assembler material on-line. We've received a lot of e-mail where each people talk about different aspects about this material. We've tried to put these comments and suggestions in this update assembler material. We hope that this new Assembler material release reach to all people that they interest to learn the most important language for IBM PC.

In this new assembler release includes:

A complete chapter about how to use debug program
More example of the assembler material
Each section of this assembler material includes a link file to Free
On-line of Computing by Dennis Howe
Finally, a search engine to look for any topic or item related with this updated material.

1.2 Presentation

The document you are looking at, has the primordial function of introducing you to assembly language programming, and it has been thought for those people who have never worked with this language.

The tutorial is completely focused towards the computers that function with processors of the x86 family of Intel, and considering that the language bases its functioning on the internal resources of the processor, the described examples are not compatible with any other architecture.

The information was structured in units in order to allow easy access to each of the topics and facilitate the following of the tutorial.

In the introductory section some of the elemental concepts regarding computer systems are mentioned, along with the concepts of the assembly language itself, and continues with the tutorial itself.

1.3 Why learn assembler language

The first reason to work with assembler is that it provides the opportunity of knowing more the operation of your PC, which allows the development of software in a more consistent manner.

The second reason is the total control of the PC which you can have with the use of the assembler.

Another reason is that the assembly programs are quicker, smaller, and have
larger capacities than ones created with other languages.

Lastly, the assembler allows an ideal optimization in programs, be it on their size or on their execution.

1.4 We need your opinion

Our goal is offers you easier way to learn yourself assembler language. You send us your comments or suggestions about this 96' edition. Any comment will be welcome.

2. Basic Concepts

Contents

2.1 Basic description of a computer system.
2.2 Assembler language Basic concepts
2.3 Using debug program

2.1 Basic description of a computer system.

This section has the purpose of giving a brief outline of the main components of a computer system at a basic level, which will allow the user a greater understanding of the concepts which will be dealt with throughout the tutorial.

Contents

2.1.1 Central Processor
2.1.2 Central Memory
2.1.3 Input and Output Units
2.1.4 Auxiliary Memory Units

 Computer System.

We call computer system to the complete configuration of a computer, including the peripheral units and the system programming which make it a useful and functional machine for a determined task.

2.1.1 Central Processor.

This part is also known as central processing unit or CPU, which in turn is made by the control unit and the arithmetic and logic unit. Its functions consist in reading and writing the contents of the memory cells, to forward data between memory cells and special registers, and decode and execute the instructions of a program. The processor has a series of memory cells which are used very often and thus, are part of the CPU. These cells are known with the name of registers. A processor may have one or two dozen of these registers. The arithmetic and logic unit of the CPU realizes the operations related with numeric and symbolic calculations. Typically these units only have capacity of performing very elemental operations such as: the addition and subtraction of two whole numbers, whole number multiplication and division, handling of the registers' bits and the comparison of the content of two registers. Personal computers can be classified by what is known as word size, this is, the quantity of bits which the processor can handle at a time.

2.1.2 Central Memory.

It is a group of cells, now being fabricated with semi-conductors, used for general processes, such as the execution of programs and the storage of information for the operations.

Each one of these cells may contain a numeric value and they have the property of being addressable, this is, that they can distinguish one from another by means of a unique number or an address for each cell.

The generic name of these memories is Random Access Memory or RAM. The main disadvantage of this type of memory is that the integrated circuits lose the information they have stored when the electricity flow is interrupted. This was the reason for the creation of memories whose information is not lost when the system is turned off. These memories receive the name of Read Only Memory or ROM.

2.1.3 Input and Output Units.

In order for a computer to be useful to us it is necessary that the processor communicates with the exterior through interfaces which allow the input and output of information from the processor and the memory. Through the use of these communications it is possible to introduce information to be processed and to later visualize the processed data.

Some of the most common input units are keyboards and mice. The most common output units are screens and printers.

2.1.4 Auxiliary Memory Units.

Since the central memory of a computer is costly, and considering today's applications it is also very limited. Thus, the need to create practical and economical information storage systems arises. Besides, the central memory loses its content when the machine is turned off, therefore making it inconvenient for the permanent storage of data.

These and other inconvenience give place for the creation of peripheral units of memory which receive the name of auxiliary or secondary memory. Of these the most common are the tapes and magnetic discs.

The stored information on these magnetic media means receive the name of files. A file is made of a variable number of registers, generally of a fixed size; the registers may contain information or programs.

2.2 Assembler language Basic concepts

Contents

2.2.1 Information in the computers
2.2.2 Data representation methods

2.2.1 Information in the computer

Contents

2.2.1.1 Information units
2.2.1.2 Numeric systems
2.2.1.3 Converting binary numbers to decimal
2.2.1.4 Converting decimal numbers to binary
2.2.1.5 Hexadecimal system

 2.2.1.1 Information Units

In order for the PC to process information, it is necessary that this information be in special cells called registers. The registers are groups of 8 or 16 flip-flops.

A flip-flop is a device capable of storing two levels of voltage, a low one, regularly 0.5 volts, and another one, commonly of 5 volts. The low level of energy in the flip-flop is interpreted as off or 0, and the high level as on or 1. These states are usually known as bits, which are the smallest information unit in a computer.

A group of 16 bits is known as word; a word can be divided in groups of 8 bits called bytes, and the groups of 4 bits are called nibbles.

2.2.1.2 Numeric systems

The numeric system we use daily is the decimal system, but this system is not convenient for machines since the information is handled codified in the shape of on or off bits; this way of codifying takes us to the necessity of knowing the positional calculation which will allow us to express a number in any base where we need it.

It is possible to represent a determined number in any base through the following formula:

Where n is the position of the digit beginning from right to left and numbering from zero. D is the digit on which we operate and B is the used numeric base.

2.2.1.3 converting binary numbers to decimals

When working with assembly language we come on the necessity of converting numbers from the binary system, which is used by computers, to the decimal
system used by people.

The binary system is based on only two conditions or states, be it on(1) or off(0), thus its base is two.

For the conversion we can use the positional value formula:

For example, if we have the binary number of 10011, we take each digit from right to left and multiply it by the base, elevated to the new position they are:

Binary: 1 1 0 0 1

Decimal: 1*2^0 + 1*2^1 + 0*2^2 + 0*2^3 + 1*2^4

= 1 + 2 + 0 + 0 + 16 = 19 decimal.

The ^ character is used in computation as an exponent symbol and the * character is used to represent multiplication.

2.2.1.4 Converting decimal numbers to binary

There are several methods to convert decimal numbers to binary; only one
will be analyzed here. Naturally a conversion with a scientific calculator is much easier, but one cannot always count with one, so it is convenient to at least know one formula to do it.

The method that will be explained uses the successive division of two, keeping the residue as a binary digit and the result as the next number to divide.

Let us take for example the decimal number of 43.

43/2=21 and its residue is 1

21/2=10 and its residue is 1

10/2=5 and its residue is 0

5/2=2 and its residue is 1

2/2=1 and its residue is 0

1/2=0 and its residue is 1

Building the number from the bottom , we get that the binary result is
101011

2.2.1.5 Hexadecimal system

On the hexadecimal base we have 16 digits which go from 0 to 9 and from the letter A to the F, these letters represent the numbers from 10 to 15. Thus we count 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E, and F.

The conversion between binary and hexadecimal numbers is easy. The first thing done to do a conversion of a binary number to a hexadecimal is to divide it in groups of 4 bits, beginning from the right to the left. In case the last group, the one most to the left, is under 4 bits, the missing places are filled with zeros.

Taking as an example the binary number of 101011, we divide it in 4 bits groups and we are left with:

10;1011

Filling the last group with zeros (the one from the left):

0010;1011

Afterwards we take each group as an independent number and we consider its
decimal value:

0010=2;1011=11

But since we cannot represent this hexadecimal number as 211 because it would be an error, we have to substitute all the values greater than 9 by their respective representation in hexadecimal, with which we obtain:

2BH, where the H represents the hexadecimal base.

In order to convert a hexadecimal number to binary it is only necessary to invert the steps: the first hexadecimal digit is taken and converted to binary, and then the second, and so on.

2.2.2 Data representation methods in a computer.

Contents

 2.2.2.1.ASCII code
2.2.2.2 BCD method
2.2.2.3 Floating point representation

2.2.2.1 ASCII code

ASCII is an acronym of American Standard Code for Information Interchange. This code assigns the letters of the alphabet, decimal digits from 0 to 9 and some additional symbols a binary number of 7 bits, putting the 8th bit in its off state or 0. This way each letter, digit or special character occupies one byte in the computer memory.

We can observe that this method of data representation is very inefficient on the numeric aspect, since in binary format one byte is not enough to represent numbers from 0 to 255, but on the other hand with the ASCII code one byte may represent only one digit. Due to this inefficiency, the ASCII code is mainly used in the memory to represent text.

2.2.2.2 BCD Method

BCD is an acronym of Binary Coded Decimal. In this notation groups of 4 bits are used to represent each decimal digit from 0 to 9. With this method we can represent two digits per byte of information.

Even when this method is much more practical for number representation in the memory compared to the ASCII code, it still less practical than the binary since with the BCD method we can only represent digits from 0 to 99. On the other hand in binary format we can represent all digits from 0 to 255.

This format is mainly used to represent very large numbers in mercantile applications since it facilitates operations avoiding mistakes.

2.2.2.3 Floating point representation

This representation is based on scientific notation, this is, to represent a number in two parts: its base and its exponent.

As an example, the number 1234000, can be represented as 1.123*10^6, in this last notation the exponent indicates to us the number of spaces that the decimal point must be moved to the right to obtain the original result.

In case the exponent was negative, it would be indicating to us the number of spaces that the decimal point must be moved to the left to obtain the original result.

2.3 Using Debug program

Contents

2.3.1 Program creation process
2.3.2 CPU registers
2.3.3 Debug program
2.3.4 Assembler structure
2.3.5 Creating basic assembler program
2.3.6 Storing and loading the programs
2.3.7 More debug program examples

2.31 Program creation process

For the creation of a program it is necessary to follow five steps:

Design of the algorithm, stage the problem to be solved is established and the best solution is proposed, creating squematic diagrams used for the better solution proposal.

Coding the algorithm, consists in writing the program in some programming language; assembly language in this specific case, taking as a base the proposed solution on the prior step.

Translation to machine language, is the creation of the object program, in other words, the written program as a sequence of zeros and ones that can be interpreted by the processor.

Test the program, after the translation the program into machine language, execute the program in the computer machine.

The last stage is the elimination of detected faults on the program on the test stage. The correction of a fault normally requires the repetition of all the steps from the first or second.

2.3.2 CPU Registers

The CPU has 4 internal registers, each one of 16 bits. The first four, AX, BX, CX, and DX are general use registers and can also be used as 8 bit registers, if used in such a way it is necessary to refer to them for example as: AH and AL, which are the high and low bytes of the AX register. This nomenclature is also applicable to the BX, CX, and DX registers.

The registers known by their specific names:

AX Accumulator
BX Base register
CX Counting register
DX Data register
DS Data Segment register
ES Extra Segment register
SS Battery segment register
CS Code Segment register
BP Base Pointers register
SI Source Index register
DI Destiny Index register
SP Battery pointer register
IP Next Instruction Pointer register
F Flag register

2.3.3 Debug program

To create a program in assembler two options exist, the first one is to use the TASM or Turbo Assembler, of Borland, and the second one is to use the debugger - on this first section we will use this last one since it is found in any PC with the MS-DOS, which makes it available to any user who has access to a machine with these characteristics.

Debug can only create files with a .COM extension, and because of the characteristics of these kinds of programs they cannot be larger that 64 kb, and they also must start with displacement, offset, or 0100H memory direction inside the specific segment.

Debug provides a set of commands that lets you perform a number of useful operations:

A Assemble symbolic instructions into machine code
D Display the contents of an area of memory
E Enter data into memory, beginning at a specific location
G Run the executable program in memory
N Name a program
P Proceed, or execute a set of related instructions
Q Quit the debug program
R Display the contents of one or more registers
T Trace the contents of one instruction
U Unassembled machine code into symbolic code
W Write a program onto disk

It is possible to visualize the values of the internal registers of the CPU using the Debug program. To begin working with Debug, type the following prompt in your computer:

C:/>Debug [Enter]

On the next line a dash will appear, this is the indicator of Debug, at this moment the instructions of Debug can be introduced using the following command:

-r[Enter]

AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0D62 ES=0D62 SS=0D62 CS=0D62 IP=0100 NV EI PL NZ NA PO NC
0D62:0100 2E CS:
0D62:0101 803ED3DF00 CMP BYTE PTR [DFD3],00 CS:DFD3=03

All the contents of the internal registers of the CPU are displayed; an alternative of viewing them is to use the "r" command using as a parameter the name of the register whose value wants to be seen. For example:

-rbx
BX 0000
:

This instruction will only display the content of the BX register and the Debug indicator changes from "-" to ":"

When the prompt is like this, it is possible to change the value of the register which was seen by typing the new value and [Enter], or the old value can be left by pressing [Enter] without typing any other value.

2.3.4 Assembler structure

In assembly language code lines have two parts, the first one is the name of the instruction which is to be executed, and the second one are the parameters of the command. For example:

add ah bh

Here "add" is the command to be executed, in this case an addition, and "ah" as well as "bh" are the parameters.

For example:

mov al, 25

In the above example, we are using the instruction mov, it means move the value 25 to al register.

The name of the instructions in this language is made of two, three or four letters. These instructions are also called mnemonic names or operation codes, since they represent a function the processor will perform.

Sometimes instructions are used as follows:

add al,[170]

The brackets in the second parameter indicate to us that we are going to work with the content of the memory cell number 170 and not with the 170 value, this is known as direct addressing.

2.3.5 Creating basic assembler program

The first step is to initiate the Debug, this step only consists of typing debug[Enter] on the operative system prompt.

To assemble a program on the Debug, the "a" (assemble) command is used; when this command is used, the address where you want the assembling to begin can be given as a parameter, if the parameter is omitted the assembling will be initiated at the locality specified by CS:IP, usually 0100h, which is the locality where programs with .COM extension must be initiated. And it will be the place we will use since only Debug can create this specific type of programs.

Even though at this moment it is not necessary to give the "a" command a parameter, it is recommendable to do so to avoid problems once the CS:IP registers are used, therefore we type:

a 100[enter]
mov ax,0002[enter]
mov bx,0004[enter]
add ax,bx[enter]
nop[enter][enter]

What does the program do?, move the value 0002 to the ax register, move the value 0004 to the bx register, add the contents of the ax and bx registers, the instruction, no operation, to finish the program.

In the debug program. After this is done, the screen will produce the following lines:

C:\>debug
-a 100
0D62:0100 mov ax,0002
0D62:0103 mov bx,0004
0D62:0106 add ax,bx
0D62:0108 nop
0D62:0109

Type the command "t" (trace), to execute each instruction of this program, example:

-t

AX=0002 BX=0000 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0D62 ES=0D62 SS=0D62 CS=0D62 IP=0103 NV EI PL NZ NA PO NC
0D62:0103 BB0400 MOV BX,0004

You see that the value 2 move to AX register. Type the command "t" (trace), again, and you see the second instruction is executed.

-t

AX=0002 BX=0004 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0D62 ES=0D62 SS=0D62 CS=0D62 IP=0106 NV EI PL NZ NA PO NC
0D62:0106 01D8 ADD AX,BX

Type the command "t" (trace) to see the instruction add is executed, you will see the follow lines:

-t

AX=0006 BX=0004 CX=0000 DX=0000 SP=FFEE BP=0000 SI=0000 DI=0000
DS=0D62 ES=0D62 SS=0D62 CS=0D62 IP=0108 NV EI PL NZ NA PE NC
0D62:0108 90 NOP

The possibility that the registers contain different values exists, but AX and BX must be the same, since they are the ones we just modified.

To exit Debug use the "q" (quit) command.

2.3.6 Storing and loading the programs

It would not seem practical to type an entire program each time it is needed, and to avoid this it is possible to store a program on the disk, with the enormous advantage that by being already assembled it will not be necessary to run Debug again to execute it.

The steps to save a program that it is already stored on memory are:

Obtain the length of the program subtracting the final address
from the initial address, naturally in hexadecimal system.

Give the program a name and extension.

Put the length of the program on the CX register.

Order Debug to write the program on the disk.

By using as an example the following program, we will have a clearer idea
of how to take these steps:

When the program is finally assembled it would look like this:

0C1B:0100 mov ax,0002
0C1B:0103 mov bx,0004
0C1B:0106 add ax,bx
0C1B:0108 int 20
0C1B:010A

To obtain the length of a program the "h" command is used, since it will show us the addition and subtraction of two numbers in hexadecimal. To obtain the length of ours, we give it as parameters the value of our program's final address (10A), and the program's initial address (100). The first result the command shows us is the addition of the parameters and the second is the subtraction.

-h 10a 100
020a 000a

The "n" command allows us to name the program.

-n test.com

The "rcx" command allows us to change the content of the CX register to the value we obtained from the size of the file with "h", in this case 000a, since the result of the subtraction of the final address from the initial address.

-rcx
CX 0000
:000a

Lastly, the "w" command writes our program on the disk, indicating how many bytes it wrote.

-w
Writing 000A bytes

To save an already loaded file two steps are necessary:

Give the name of the file to be loaded.
Load it using the "l" (load) command.

To obtain the correct result of the following steps, it is necessary that the above program be already created.

Inside Debug we write the following:

-n test.com
-l
-u 100 109
0C3D:0100 B80200 MOV AX,0002
0C3D:0103 BB0400 MOV BX,0004
0C3D:0106 01D8 ADD AX,BX
0C3D:0108 CD20 INT 20

The last "u" command is used to verify that the program was loaded on memory. What it does is that it disassembles the code and shows it disassembled. The parameters indicate to Debug from where and to where to disassemble.

Debug always loads the programs on memory on the address 100H, otherwise indicated.

3 Assembler programming

Contents

3.1 Building Assembler programs
3.2 Assembly process
3.3 More assembler programs
3.4 Types of instructions

3.1 Building Assembler programs

Contents

3.1.1 Needed software
3.1.2 Assembler Programming

 3.1.1 Needed software

In order to be able to create a program, several tools are needed:

First an editor to create the source program. Second a compiler, which is nothing more than a program that "translates" the source program into an object program. And third, a linker that generates the executable program from the object program.

The editor can be any text editor at hand, and as a compiler we will use the TASM macro assembler from Borland, and as a linker we will use the Tlink program.

The extension used so that TASM recognizes the source programs in assembler is .ASM; once translated the source program, the TASM creates a file with the .OBJ extension, this file contains an "intermediate format" of the program, called like this because it is not executable yet but it is not a program in source language either anymore. The linker generates, from a .OBJ or a combination of several of these files, an executable program, whose extension usually is .EXE though it can also be .COM, depending of the form it was assembled.

3.1.2 Assembler Programming

To build assembler programs using TASM programs is a different program structure than from using debug program.

It's important to include the following assembler directives:

.MODEL SMALL
Assembler directive that defines the memory model to use in the program

.CODE
Assembler directive that defines the program instructions

.STACK
Assembler directive that reserves a memory space for program instructions
in the stack

END
Assembler directive that finishes the assembler program

Let's program

First step

use any editor program to create the source file. Type the following lines:

First example

; use ; to put comments in the assembler program
.MODEL SMALL; memory model
.STACK; memory space for program instructions in the stack
.CODE; the following lines are program instructions
mov ah,1h; moves the value 1h to register ah
mov cx,07h; moves the value 07h to register cx
int 10h;10h interruption
mov ah,4ch; moves the value 4 ch to register ah
int 21h; 21h interruption
END; finishes the program code

This assembler program changes the size of the computer cursor.

Second step

Save the file with the following name: examp1.asm
Don't forget to save this in ASCII format.

Third step

Use the TASM program to build the object program.

Example:

C:\>tasm exam1.asm
Turbo Assembler Version 2.0 Copyright (c) 1988, 1990 Borland International

Assembling file: exam1.asm
Error messages: None
Warning messages: None
Passes: 1
Remaining memory: 471k

The TASM can only create programs in .OBJ format, which are not executable by themselves, but rather it is necessary to have a linker which generates the executable code.

Fourth step

Use the TLINK program to build the executable program example:

C:\>tlink exam1.obj
Turbo Link Version 3.0 Copyright (c) 1987, 1990 Borland International

C:\>

Where exam1.obj is the name of the intermediate program, .OBJ. This generates a file directly with the name of the intermediate program and the .EXE extension.

Fifth step

Execute the executable program

C:\>exam1[enter]

Remember, this assembler program changes the size of the cursor.

Assembly process.

Segments
Table of symbols

SEGMENTS

The architecture of the x86 processors forces to the use of memory segments to manage the information, the size of these segments is of 64kb.

The reason of being of these segments is that, considering that the maximum size of a number that the processor can manage is given by a word of 16 bits or register, it would not be possible to access more than 65536 localities of memory using only one of these registers, but now, if the PC's memory is divided into groups or segments, each one of 65536 localities, and we use an address on an exclusive register to find each segment, and then we make each address of a specific slot with two registers, it is possible for us to access a quantity of 4294967296 bytes of memory, which is, in the present day, more memory than what we will see installed in a PC.

In order for the assembler to be able to manage the data, it is necessary that each piece of information or instruction be found in the area that corresponds to its respective segments. The assembler accesses this information taking into account the localization of the segment, given by the DS, ES, SS and CS registers and inside the register the address of the specified piece of information. It is because of this that when we create a program using the Debug on each line that we assemble, something like this appears:

1CB0:0102 MOV AX,BX

Where the first number, 1CB0, corresponds to the memory segment being used, the second one refers to the address inside this segment, and the instructions which will be stored from that address follow.

The way to indicate to the assembler with which of the segments we will work with is with the .CODE, .DATA and .STACK directives.

The assembler adjusts the size of the segments taking as a base the number of bytes each assembled instruction needs, since it would be a waste of memory to use the whole segments. For example, if a program only needs 10kb to store data, the data segment will only be of 10kb and not the 64kb it can handle.

SYMBOLS CHART

Each one of the parts on code line in assembler is known as token, for example on the code line:

MOV AX,Var

we have three tokens, the MOV instruction, the AX operator, and the VAR operator. What the assembler does to generate the OBJ code is to read each one of the tokens and look for it on an internal "equivalence" chart known as the reserved words chart, which is where all the mnemonic meanings we use as instructions are found.

Following this process, the assembler reads MOV, looks for it on its chart and identifies it as a processor instruction. Likewise it reads AX and recognizes it as a register of the processor, but when it looks for the Var token on the reserved words chart, it does not find it, so then it looks for it on the symbols chart which is a table where the names of the variables, constants and labels used in the program where their addresses on memory are included and the sort of data it contains, are found.

Sometimes the assembler comes on a token which is not defined on the program, therefore what it does in these cased is to pass a second time by the source program to verify all references to that symbol and place it on the symbols chart.

There are symbols which the assembler will not find since they do not belong to that segment and the program does not know in what part of the memory it will find that segment, and at this time the linker comes into action, which will create the structure necessary for the loader so that the segment and the token be defined when the program is loaded and before it is executed.

3.3 More assembler programs

Another example

First step

Use any editor program to create the source file. Type the following lines:

;example11
.model small
.stack
.code
mov ah,2h ;moves the value 2h to register ah
mov dl,2ah ;moves de value 2ah to register dl
;(Its the asterisk value in ASCII format)
int 21h ;21h interruption
mov ah,4ch ;4ch function, goes to operating system
int 21h ;21h interruption
end ;finishes the program code

Second step

Save the file with the following name: exam2.asm
Don't forget to save this in ASCII format.

Third step

Use the TASM program to build the object program.

C:\>tasm exam2.asm
Turbo Assembler Version 2.0 Copyright (c) 1988, 1990 Borland International

Assembling file: exam2.asm
Error messages: None
Warning messages: None
Passes: 1
Remaining memory: 471k

Fourth step

Use the TLINK program to build the executable program

C:\>tlink exam2.obj
Turbo Link Version 3.0 Copyright (c) 1987, 1990 Borland International

C:\>

Fifth step

Execute the executable program

C:\>ejem11[enter]
*
C:\>

This assembler program shows the asterisk character on the computer screen

3.4 Types of instructions.

Contents

3.4.1 Data movement
3.4.2 Logic and arithmetic operations
3.4.3 Jumps, loops and procedures

3.4.1 Data movement

In any program it is necessary to move the data in the memory and in the CPU registers; there are several ways to do this: it can copy data in the memory to some register, from register to register, from a register to a stack, from a stack to a register, to transmit data to external devices as well as vice versa.

This movement of data is subject to rules and restrictions. The following are some of them:

*It is not possible to move data from a memory locality to another directly; it is necessary to first move the data of the origin locality to a register and then from the register to the destiny locality.

*It is not possible to move a constant directly to a segment register; it first must be moved to a register in the CPU.

It is possible to move data blocks by means of the movs instructions, which copies a chain of bytes or words; movsb which copies n bytes from a locality to another; and movsw copies n words from a locality to another. The last two instructions take the values from the defined addresses by DS:SI as a group of data to move and ES:DI as the new localization of the data.

To move data there are also structures called batteries, where the data is introduced with the push instruction and are extracted with the pop instruction. In a stack the first data to be introduced is the last one we can take, this is, if in our program we use these instructions:

PUSH AX
PUSH BX
PUSH CX

To return the correct values to each register at the moment of taking them from the stack it is necessary to do it in the following order:

POP CX
POP BX
POP AX

For the communication with external devices the out command is used to send information to a port and the in command to read the information received from a port.

The syntax of the out command is:

OUT DX,AX

Where DX contains the value of the port which will be used for the communication and AX contains the information which will be sent.

The syntax of the in command is:

IN AX,DX

Where AX is the register where the incoming information will be kept and DX contains the address of the port by which the information will arrive.

3.4.2 Logic and arithmetic operations

The instructions of the logic operations are: and, not, or and xor. These work on the bits of their operators. To verify the result of the operations we turn to the cmp and test instructions. The instructions used for the algebraic operations are: to add, to subtract sub, to multiply mul and to divide div.

Almost all the comparison instructions are based on the information contained in the flag register. Normally the flags of this register which can be directly handled by the programmer are the data direction flag DF, used to define the operations about chains.

Another one which can also be handled is the IF flag by means of the sti and cli instructions, to activate and deactivate the interruptions.

3.4.3 Jumps, loops and procedures

The unconditional jumps in a written program in assembler language are given by the jmp instruction; a jump is to moves the flow of the execution of a program by sending the control to the indicated address.

A loop, known also as iteration, is the repetition of a process a certain number of times until a condition is fulfilled. These loops are used (broken sentence).

4 Assembler language Instructions

Contents

4.1 Transfer instructions
4.2 Loading instructions
4.3 Stack instructions
4.4 Logic instructions
4.5 Arithmetic instructions
4.6 Jump instructions
4.7 Instructions for cycles: loop
4.8 Counting Instructions
4.9 Comparison Instructions
4.10 Flag Instructions

 4.1 Transfer instructions

They are used to move the contents of the operators. Each instruction can be used with different modes of addressing.

MOV
MOVS (MOVSB) (MOVSW)

MOV INSTRUCTION

Purpose: Data transfer between memory cells, registers and the accumulator.

Syntax:

MOV Destiny, Source

Where Destiny is the place where the data will be moved and Source is the place where the data is.

The different movements of data allowed for this instruction are:

*Destiny: memory. Source: accumulator
*Destiny: accumulator. Source: memory
*Destiny: segment register. Source: memory/register
*Destiny: memory/register. Source: segment register
*Destiny: register. Source: register
*Destiny: register. Source: memory
*Destiny: memory. Source: register
*Destiny: register. Source: immediate data
*Destiny: memory. Source: immediate data

Example:

MOV AX,0006h
MOV BX,AX
MOV AX,4C00h
INT 21H

This small program moves the value of 0006H to the AX register, then it moves the content of AX (0006h) to the BX register, and lastly it moves the 4C00h value to the AX register to end the execution with the 4C option of the 21h interruption.

MOVS (MOVSB) (MOVSW) Instruction

Purpose: To move byte or word chains from the source, addressed by SI, to
the destiny addressed by DI.

Syntax:

MOVS

This command does not need parameters since it takes as source address the
content of the SI register and as destination the content of DI. The following sequence of instructions illustrates this:

MOV SI, OFFSET VAR1
MOV DI, OFFSET VAR2
MOVS

First we initialize the values of SI and DI with the addresses of the VAR1 and VAR2 variables respectively, then after executing MOVS the content of VAR1 is copied onto VAR2.

The MOVSB and MOVSW are used in the same way as MOVS, the first one moves one byte and the second one moves a word.

4.2 Loading instructions

They are specific register instructions. They are used to load bytes or chains of bytes onto a register.

LODS (LODSB) (LODSW)
LAHF
LDS
LEA
LES

LODS (LODSB) (LODSW) INSTRUCTION

Purpose: To load chains of a byte or a word into the accumulator.

Syntax:

LODS

This instruction takes the chain found on the address specified by SI, loads it to the AL (or AX) register and adds or subtracts , depending on the state of DF, to SI if it is a bytes transfer or if it is a words transfer.

MOV SI, OFFSET VAR1
LODS

The first line loads the VAR1 address on SI and the second line takes the content of that locality to the AL register.

The LODSB and LODSW commands are used in the same way, the first one loads a byte and the second one a word (it uses the complete AX register).

LAHF INSTRUCTION

Purpose: It transfers the content of the flags to the AH register.

Syntax:

LAHF

This instruction is useful to verify the state of the flags during the execution of our program.

The flags are left in the following order inside the register:

SF ZF ?? AF ?? PF ?? CF

The "??" means that there will be an undefined value in those bits.

LDS INSTRUCTION

Purpose: To load the register of the data segment

Syntax:

LDS destiny, source

The source operator must be a double word in memory. The word associated with the largest address is transferred to DS, in other words it is taken as the segment address. The word associated with the smaller address is the displacement address and it is deposited in the register indicated as destiny.

LEA INSTRUCTION

Purpose: To load the address of the source operator

Syntax:

LEA destiny, source

The source operator must be located in memory, and its displacement is placed on the index register or specified pointer in destiny.

To illustrate one of the facilities we have with this command let us write an equivalence:

MOV SI,OFFSET VAR1

Is equivalent to:

LEA SI,VAR1

It is very probable that for the programmer it is much easier to create extensive programs by using this last format.

LES INSTRUCTION

Purpose: To load the register of the extra segment

Syntax:

LES destiny, source

The source operator must be a double word operator in memory. The content of the word with the larger address is interpreted as the segment address and it is placed in ES. The word with the smaller address is the displacement address and it is placed in the specified register on the destiny parameter.

4.3 Stack instructions

These instructions allow the use of the stack to store or retrieve data.

POP
POPF
PUSH
PUSHF

POP INSTRUCTION

Purpose: It recovers a piece of information from the stack

Syntax:

POP destiny

This instruction transfers the last value stored on the stack to the destiny operator, it then increases by 2 the SP register.

This increase is due to the fact that the stack grows from the highest memory segment address to the lowest, and the stack only works with words, 2 bytes, so then by increasing by two the SP register, in reality two are being subtracted from the real size of the stack.

POPF INSTRUCTION

Purpose: It extracts the flags stored on the stack

Syntax:

POPF

This command transfers bits of the word stored on the higher part of the stack to the flag register.

The way of transference is as follows:

BIT FLAG

0 CF
2 PF
4 AF
6 ZF
7 SF
8 TF
9 IF
10 DF
11 OF

These localities are the same for the PUSHF command.

Once the transference is done, the SP register is increased by 2, diminishing the size of the stack.

PUSH INSTRUCTION

Purpose: It places a word on the stack.

Syntax:

PUSH source

The PUSH instruction decreases by two the value of SP and then transfers the content of the source operator to the new resulting address on the recently modified register.

The decrease on the address is due to the fact that when adding values to the stack, this one grows from the greater to the smaller segment address, therefore by subtracting 2 from the SP register what we do is to increase the size of the stack by two bytes, which is the only quantity of information the stack can handle on each input and output of information.

PUSHF INSTRUCTION

Purpose: It places the value of the flags on the stack.

Syntax:

PUSHF

This command decreases by 2 the value of the SP register and then the content of the flag register is transferred to the stack, on the address indicated by SP.

The flags are left stored in memory on the same bits indicated on the POPF command.

4.4 Logic instructions

They are used to perform logic operations on the operators.

AND
NEG
NOT
OR
TEST
XOR

 AND INSTRUCTION

Purpose: It performs the conjunction of the operators bit by bit.

Syntax:

AND destiny, source

With this instruction the "y" logic operation for both operators is carried out:

Source Destiny | Destiny
1 1 | 1
1 0 | 0
0 1 | 0
0 0 | 0

The result of this operation is stored on the destiny operator.

NEG INSTRUCTION

Purpose: It generates the complement to 2.

Syntax:

NEG destiny

This instruction generates the complement to 2 of the destiny operator and stores it on the same operator.

For example, if AX stores the value of 1234H, then:

NEG AX

This would leave the EDCCH value stored on the AX register.

NOT INSTRUCTION

Purpose: It carries out the negation of the destiny operator bit by bit.

Syntax:

NOT destiny

The result is stored on the same destiny operator.

OR INSTRUCTION

Purpose: Logic inclusive OR

Syntax:

OR destiny, source

The OR instruction carries out, bit by bit, the logic inclusive disjunction of the two operators:

Source Destiny | Destiny
 1 1 | 1
1 0 | 1
0 1 | 1
0 0 | 0

TEST INSTRUCTION

Purpose: It logically compares the operators

Syntax:

TEST destiny, source

It performs a conjunction, bit by bit, of the operators, but differing from AND, this instruction does not place the result on the destiny operator, it only has effect on the state of the flags.

XOR INSTRUCTION

Purpose: OR exclusive

Syntax:

XOR destiny, source

Its function is to perform the logic exclusive disjunction of the two operators bit by bit.

Source Destiny | Destiny
1 1 | 0
0 0 | 1
0 1 | 1
0 0 | 0

4.5 Arithmetic instructions

They are used to perform arithmetic operations on the operators.

ADC
ADD
DIV
IDIV
MUL
IMUL
SBB
SUB

 ADC INSTRUCTION

Purpose: Cartage addition

Syntax:

ADC destiny, source

It carries out the addition of two operators and adds one to the result in case the CF flag is activated, this is in case there is carried.

The result is stored on the destiny operator.

ADD INSTRUCTION

Purpose: Addition of the operators.

Syntax:

ADD destiny, source

It adds the two operators and stores the result on the destiny operator.

DIV INSTRUCTION

Purpose: Division without sign.

Syntax:

DIV source

The divider can be a byte or a word and it is the operator which is given the instruction.

If the divider is 8 bits, the 16 bits AX register is taken as dividend and if the divider is 16 bits the even DX:AX register will be taken as dividend, taking the DX high word and AX as the low.

If the divider was a byte then the quotient will be stored on the AL register and the residue on AH, if it was a word then the quotient is stored on AX and the residue on DX.

IDIV INSTRUCTION

Purpose: Division with sign.

Syntax:

IDIV source

It basically consists on the same as the DIV instruction, and the only difference is that this one performs the operation with sign.

For its results it used the same registers as the DIV instruction.

MUL INSTRUCTION

Purpose: Multiplication with sign.

Syntax:

MUL source

The assembler assumes that the multiplicand will be of the same size as the
multiplier, therefore it multiplies the value stored on the register given as operator by the one found to be contained in AH if the multiplier is 8 bits or by AX if the multiplier is 16 bits.

When a multiplication is done with 8 bit values, the result is stored on the AX register and when the multiplication is with 16 bit values the result is stored on the even DX:AX register.

IMUL INSTRUCTION

Purpose: Multiplication of two whole numbers with sign.

Syntax:

IMUL source

This command does the same as the one before, only that this one does take
into account the signs of the numbers being multiplied.

The results are kept in the same registers that the MOV instruction uses.

SBB INSTRUCTION

Purpose: Subtraction with cartage.

Syntax:

SBB destiny, source

This instruction subtracts the operators and subtracts one to the result if CF is activated. The source operator is always subtracted from the destiny.

This kind of subtraction is used when one is working with 32 bits quantities.

SUB INSTRUCTION

Purpose: Subtraction.

Syntax:

SUB destiny, source

It subtracts the source operator from the destiny.

4.6 Jump instructions

They are used to transfer the flow of the process to the indicated
operator.

JMP
JA (JNBE)
JAE (JNBE)
JB (JNAE)
JBE (JNA)
JE (JZ)
JNE (JNZ)
JG (JNLE)
JGE (JNL)
JL (JNGE)
JLE (JNG)
JC
JNC
JNO
JNP (JPO)
JNS
JO
JP (JPE)
JS

JMP INSTRUCTION

Purpose: Unconditional jump.

Syntax:

JMP destiny

This instruction is used to deviate the flow of a program without taking into account the actual conditions of the flags or of the data.

JA (JNBE) INSTRUCTION

Purpose: Conditional jump.

Syntax:

JA Label

After a comparison this command jumps if it is or jumps if it is not down or if not it is the equal.

This means that the jump is only done if the CF flag is deactivated or if the ZF flag is deactivated, that is that one of the two be equal to zero.

JAE (JNB) INSTRUCTION

Purpose: Conditional jump.

Syntax:

JAE label

It jumps if it is or it is the equal or if it is not down.

The jump is done if CF is deactivated.

JB (JNAE) INSTRUCTION

Purpose: Conditional jump.

Syntax:

JB label

It jumps if it is down, if it is not , or if it is the equal.

The jump is done if CF is activated.

JBE (JNA) INSTRUCTION

Purpose: Conditional jump.

Syntax:

JBE label

It jumps if it is down, the equal, or if it is not .

The jump is done if CF is activated or if ZF is activated, that any of them be equal to 1.

JE (JZ) INSTRUCTION

Purpose: Conditional jump.

Syntax:

JE label

It jumps if it is the equal or if it is zero.

The jump is done if ZF is activated.

JNE (JNZ) INSTRUCTION

Purpose: Conditional jump.

Syntax:

JNE label

It jumps if it is not equal or zero.

The jump will be done if ZF is deactivated.

JG (JNLE) INSTRUCTION

Purpose: Conditional jump, and the sign is taken into account.

Syntax:

JG label

It jumps if it is larger, if it is not larger or equal.

The jump occurs if ZF = 0 or if OF = SF.

JGE (JNL) INSTRUCTION

Purpose: Conditional jump, and the sign is taken into account.

Syntax:

JGE label

It jumps if it is larger or less than, or equal to.

The jump is done if SF = OF

JL (JNGE) INSTRUCTION

Purpose: Conditional jump, and the sign is taken into account.

Syntax:

JL label

It jumps if it is less than or if it is not larger than or equal to.

The jump is done if SF is different than OF.

JLE (JNG) INSTRUCTION

Purpose: Conditional jump, and the sign is taken into account.

Syntax:

JLE label

It jumps if it is less than or equal to, or if it is not larger.

The jump is done if ZF = 1 or if SF is defferent than OF.

JC INSTRUCTION

Purpose: Conditional jump, and the flags are taken into account.

Syntax:

JC label

It jumps if there is cartage.

The jump is done if CF = 1

JNC INSTRUCTION

Purpose: Conditional jump, and the state of the flags is taken into
account.

Syntax:

JNC label

It jumps if there is no cartage.

The jump is done if CF = 0.

JNO INSTRUCTION

Purpose: Conditional jump, and the state of the flags is taken into
account.

Syntax:

JNO label

It jumps if there is no overflow.

The jump is done if OF = 0.

JNP (JPO) INSTRUCTION

Purpose: Conditional jump, and the state of the flags is taken into
account.

Syntax:

JNP label

It jumps if there is no parity or if the parity is uneven.

The jump is done if PF = 0.

JNS INSTRUCTION

Purpose: Conditional jump, and the state of the flags is taken into
account.

Syntax:

JNP label

It jumps if the sign is deactivated.

The jump is done if SF = 0.

JO INSTRUCTION

Purpose: Conditional jump, and the state of the flags is taken into
account.

Syntax:

JO label

It jumps if there is overflow.

The jump is done if OF = 1.

JP (JPE) INSTRUCTION

Purpose: Conditional jump, the state of the flags is taken into account.

Syntax:

JP label

It jumps if there is parity or if the parity is even.

The jump is done if PF = 1.

JS INSTRUCTION

Purpose: Conditional jump, and the state of the flags is taken into
account.

Syntax:

JS label

It jumps if the sign is on.

The jump is done if SF = 1.

4.7 Instructions for cycles:loop

They transfer the process flow, conditionally or unconditionally, to a
destiny, repeating this action until the counter is zero.

LOOP
LOOPE
LOOPNE

LOOP INSTRUCTION

Purpose: To generate a cycle in the program.

Syntax:

LOOP label

The loop instruction decreases CX on 1, and transfers the flow of the
program to the label given as operator if CX is different than 1.

LOOPE INSTRUCTION

Purpose: To generate a cycle in the program considering the state of ZF.

Syntax:

LOOPE label

This instruction decreases CX by 1. If CX is different to zero and ZF is equal to 1, then the flow of the program is transferred to the label indicated as operator.

LOOPNE INSTRUCTION

Purpose: To generate a cycle in the program, considering the state of ZF.

Syntax:

LOOPNE label

This instruction decreases one from CX and transfers the flow of the program only if ZF is different to 0.

4.8 Counting instructions

They are used to decrease or increase the content of the counters.

DEC
INC

DEC INSTRUCTION

Purpose: To decrease the operator.

Syntax:

DEC destiny

This operation subtracts 1 from the destiny operator and stores the new
value in the same operator.

INC INSTRUCTION

Purpose: To increase the operator.

Syntax:

INC destiny The instruction adds 1 to the destiny operator and keeps the
result in the same destiny operator.

4.9 Comparison instructions

They are used to compare operators, and they affect the content of the
flags.

CMP
CMPS (CMPSB) (CMPSW)

 CMP INSTRUCTION

Purpose: To compare the operators.

Syntax:

CMP destiny, source

This instruction subtracts the source operator from the destiny operator but without this one storing the result of the operation, and it only affects the state of the flags.

CMPS (CMPSB) (CMPSW) INSTRUCTION

Purpose: To compare chains of a byte or a word.

Syntax:

CMP destiny, source

With this instruction the chain of source characters is subtracted from the destiny chain.

DI is used as an index for the extra segment of the source chain, and SI as an index of the destiny chain.

It only affects the content of the flags and DI as well as SI are incremented.

4.10 Flag instructions

They directly affect the content of the flags.

CLC
CLD
CLI
CMC
STC
STD
STI

CLC INSTRUCTION

Purpose: To clean the cartage flag.

Syntax:

CLC

This instruction turns off the bit corresponding to the cartage flag, or in other words it puts it on zero.

CLD INSTRUCTION

Purpose: To clean the address flag.

Syntax:

CLD

This instruction turns off the corresponding bit to the address flag.

CLI INSTRUCTION

Purpose: To clean the interruption flag.

Syntax:

CLI

This instruction turns off the interruptions flag, disabling this way those maskarable interruptions.

A maskarable interruptions is that one whose functions are deactivated when IF=0.

CMC INSTRUCTION

Purpose: To complement the cartage flag.

Syntax:

CMC

This instruction complements the state of the CF flag, if CF = 0 the instructions equals it to 1, and if the instruction is 1 it equals it to 0.

We could say that it only "inverts" the value of the flag.

STC INSTRUCTION

Purpose: To activate the cartage flag.

Syntax:

STC

This instruction puts the CF flag in 1.

STD INSTRUCTION

Purpose: To activate the address flag.

Syntax:

STD

The STD instruction puts the DF flag in 1.

STI INSTRUCTION

Purpose: To activate the interruption flag.

Syntax:

STI

The instruction activates the IF flag, and this enables the maskarable external interruptions ( the ones which only function when IF = 1).
5 Interruptions and file managing

Contents

5.1 Internal hardware interruptions
5.2 External hardware interruptions
5.3 Software interruptions
5.4 Most Common interruptions

5.1 Internal hardware interruptions

Internal interruptions are generated by certain events which come during the execution of a program.

This type of interruptions are managed on their totality by the hardware and it is not possible to modify them.

A clear example of this type of interruptions is the one which actualizes the counter of the computer internal clock, the hardware makes the call to this interruption several times during a second in order to maintain the time to date.

Even though we cannot directly manage this interruption, since we cannot control the time dating by means of software, it is possible to use its effects on the computer to our benefit, for example to create a "virtual clock" dated continuously thanks to the clock's internal counter. We only have to write a program which reads the actual value of the counter and to translates it into an understandable format for the user.

5.2 External hardware interruptions

External interruptions are generated by peripheral devices, such as keyboards, printers, communication cards, etc. They are also generated by coprocessors. It is not possible to deactivate external interruptions.

These interruptions are not sent directly to the CPU, but rather they are sent to an integrated circuit whose function is to exclusively handle this type of interruptions. The circuit, called PIC8259A, is controlled by the CPU using for this control a series of communication ways called paths.

5.3 Software interruptions

Software interruptions can be directly activated by the assembler invoking the number of the desired interruption with the INT instruction.

The use of interruptions helps us in the creation of programs, and by using them our programs are shorter, it is easier to understand them and they usually have a better performance mostly due to their smaller size.

This type of interruptions can be separated in two categories: the operative system DOS interruptions and the BIOS interruptions.

The difference between the two is that the operative system interruptions are easier to use but they are also slower since these interruptions make use of the BIOS to achieve their goal, on the other hand the BIOS interruptions are much faster but they have the disadvantage that since they are part of the hardware, they are very specific and can vary depending even on the brand of the maker of the circuit.

The election of the type of interruption to use will depend solely on the characteristics you want to give your program: speed, using the BIOS ones, or portability, using the ones from the DOS.

5.4 Most common interruptions

Contents

5.4.1 Int 21H (DOS interruption)
Multiple calls to DOS functions.

5.4.2 Int 10H (BIOS interruption)
Video input/output.

5.4.3 Int 16H (BIOS interruption)
Keyboard input/output.

5.4.4 Int 17H (BIOS interruption)
Printer input/output.

5.41 21H Interruption
Purpose: To call on diverse DOS functions.

Syntax:

Int 21H

Note: When we work in TASM program is necessary to specify that the value we
are using is hexadecimal.

This interruption has several functions, to access each one of them it is necessary that the function number which is required at the moment of calling the interruption is in the AH register.

Functions to display information to the video.

02H Exhibits output
09H Chain Impression (video)
40H Writing in device/file

Functions to read information from the keyboard.

01H Input from the keyboard
0AH Input from the keyboard using buffer
3FH Reading from device/file

Functions to work with files.

In this section only the specific task of each function is exposed, for a
reference about the concepts used, refer to unit 7, titled : "Introduction
to file handling".

FCB Method

0FH Open file
14H Sequential reading
15H Sequential writing
16H Create file
21H Random reading
22H Random writing

Handles

3CH Create file
3DH Open file
3EH Close file driver
3FH Reading from file/device
40H Writing in file/device
42H Move pointer of reading/writing in file

VIDEO DISPLAY FUNCTIONS

 02H FUNCTION

Use:

It displays one character to the screen.

Calling registers:

AH = 02H
DL = Value of the character to display.

Return registers:

None.

This function displays the character whose hexadecimal code corresponds to the value stored in the DL register, and no register is modified by using this command.

The use of the 40H function is recommended instead of this function.

09H FUNCTION

Use:

It displays a chain of characters on the screen.

Call registers:

AH = 09H
DS:DX = Address of the beginning of a chain of characters.

Return registers:

None.

This function displays the characters, one by one, from the indicated address in the DS:DX register until finding a $ character, which is interpreted as the end of the chain.

It is recommended to use the 40H function instead of this one.

40H FUNCTION

Use:

To write to a device or a file.

Call registers:

AH = 40H
BX = Path of communication
CX = Quantity of bytes to write
DS:DX = Address of the beginning of the data to write

Return registers:

CF = 0 if there was no mistake

AX = Number of bytes written

CF = 1 if there was a mistake

AX = Error code

The use of this function to display information on the screen is done by giving the BX register the value of 1 which is the preassigned value to the video by the operative system MS-DOS.

KEYBOARD INFORMATION FUNCTIONS

01H FUNCTION

Use:

To read a keyboard character and to display it.

Call registers

AH = 01H

Return registers:

AL = Read character

It is very easy to read a character from the keyboard with this function, the hexadecimal code of the read character is stored in the AL register. In case it is an extended register the AL register will contain the value of 0 and it will be necessary to call on the function again to obtain the code of that character.

0AH FUNCTION

Use:

To read keyboard characters and store them on the buffer.

Call registers:

AH = 0AH
DS:DX = Area of storage address
BYTE 0 = Quantity of bytes in the area
BYTE 1 = Quantity of bytes read
from BYTE 2 till BYTE 0 + 2 = read characters

Return characters:

None.

The characters are read and stored in a predefined space on memory. The structure of this space indicate that in the first byte are indicated how many characters will be read. On the second byte the number of characters already read are stored, and from the third byte on the read characters are written.

When all the indicated characters have been stored the speaker sounds and any additional character is ignored. To end the capture of the chain it is necessary to hit [ENTER].

3FH FUNCTION

Use:

To read information from a device or file.

Call registers:

AH = 3FH
BX = Number assigned to the device
CX = Number of bytes to process
DS:DX = Address of the storage area

Return registers:

CF = 0 if there is no error and AX = number of read bytes.
CF = 1 if there is an error and AX will contain the error code.
FILE WORKING FUNCTIONS:

FCB FUNCTIONS:
0FH FUNCTION

Use:

To open an FCB file

Call registers:

AH = 0FH
DS:DX = Pointer to an FCB

Return registers:

AL = 00H if there was no problem, otherwise it returns to 0FFH

14H FUNCTION

Use:

To sequentially read an FCB file.

Call registers:

AH = 14H
DS:DX = Pointer to an FCB already opened.

Return registers:

AL = 0 if there were no errors, otherwise the corresponding error code will be returned: 1 error at the end of the file, 2 error on the FCB structure and 3 partial reading error.

What this function does is that it reads the next block of information from the address given by DS:DX,

What this function does is that it reads the next block of information from the address given by DS:DX, and dates this register.

15H FUNCTION

Use:

To sequentially write and FCB file.

Call registers:

AH = 15H
DS:DX = Pointer to an FCB already opened.

Return registers:

AL = 00H if there were no errors, otherwise it will contain the error code: 1 full disk or read-only file, 2 error on the formation or on the specification of the FCB.

The 15H function dates the FCB after writing the register to the present
block.

16H FUNCTION

Use:

To create an FCB file.

Call registers:

AH = 16H
DS:DX = Pointer to an already opened FCB.

Return registers:

AL = 00H if there were no errors, otherwise it will contain the 0FFH value.

It is based on the information which comes on an FCB to create a file on a disk.

21H FUNCTION

Use:

To read in an random manner an FCB file.

Call registers:

AH = 21H
DS:DX = Pointer to and opened FCB.

Return registers:

A = 00H if there was no error, otherwise AH will contain the code of the error: 1 if it is the end of file, 2 if there is an FCB specification error and 3 if a partial register was read or the file pointer is at the end of the same.

This function reads the specified register by the fields of the actual block and register of an opened FCB and places the information on the DTA, Disk Transfer Area.

22H FUNCTION

Use:

To write in an random manner an FCB file.

Call registers:

AH = 22H
DS:DX = Pointer to an opened FCB.

Return registers:

AL = 00H if there was no error, otherwise it will contain the error code: 1 if the disk is full or the file is an only read and 2 if there is an error on the

It writes the register specified by the fields of the actual block and register of an opened FCB. It writes this information from the content of the DTA.

FILE WORKING FUNCTIONS:

HANDLES:

3CH FUNCTION

Use:

To create a file if it does not exist or leave it on 0 length if it exists,
Handle.

Call registers:

AH = 3CH
CH = File attribute
DS:DX = Pointer to an ASCII specification.

Return registers:
CF = 0 and AX the assigned number to handle if there is no error, in case there is, CF will be 1 and AX will contain the error code: 3 path not found, 4 there
CF will be 1 and AX will contain the error code: 3 path not found, 4 there are no handles available to assign and 5 access denied.

This function substitutes the 16H function. The name of the file is specified on an ASCII chain, which has as a characteristic being a conventional chain of bytes ended with a 0 character.

The file created will contain the attributes defined on the CX register in the following manner:

Value Attributes
00H Normal
02H Hidden
04H System
06H Hidden and of system

The file is created with the reading and writing permissions. It is not possible to create directories using this function.

3DH FUNCTION

Use:

It opens a file and returns a handle.

Call registers:

AH = 3DH
AL = manner of access
DS:DX = Pointer to an ASCII specification

Return registers:

CF = 0 and AX = handle number if there are no errors, otherwise CF = 1 and AX = error code: 01H if the function is not valid, 02H if the file was not found, 03H if the path was not found, 04H if there are no available handles, 05H in case access is denied, and 0CH if the access code is not valid.

The returned handled is 16 bits.

The access code is specified in the following way:

BITS

 7 6 5 4 3 2 1

. . . . 0 0 0 Only reading
. . . . 0 0 1 Only writing
. . . . 0 1 0 Reading/Writing
. . . x . . . RESERVED

3EH FUNCTION

Use:

Close file (handle).

Call registers:

AH = 3EH
BX = Assigned handle

Return registers:

CF = 0 if there were no mistakes, otherwise CF will be 1 and AX will contain the error code: 06H if the handle is invalid.

This function dates the file and frees the handle it was using.

3FH FUNCTION

Use:

To read a specific quantity of bytes from an open file and store them on a specific buffer.

5.4.2 10H INTERRUPTION

Purpose: To call on diverse BIOS video function

Syntax:

Int 10H

This interruption has several functions, all of them control the video input/output, to access each one of them it is necessary that the function number which is required at the moment of calling the interruption is in the Ah register.

In this tutorial we will see some functions of the 10h interruption.

Common functions of the 10h interruption

02H Function, select the cursor position
09H Function, write attribute and character of the cursor
0AH Function, write a character in the cursor position
0EH Function, Alphanumeric model of the writing characters

02H FUNCTION

Use:

Moves the cursor on the computer screen using text model.

Call registers:

AH = 02H

BH = Video page where the cursor is positioned.

DH = row

DL = Column

Return Registers:

None.

The cursor position is defined by its coordinates, starting from the position 0,0 to position 79,24. This means from the left per computer screen corner to right lower computer screen. Therefore the numeric values that the DH and DL registers get in text model are: from 0 to 24 for rows and from 0 to 79 for columns.

09H FUNCTION

Use:

Shows a defined character several times on the computer screen with a defined attribute, starting with the actual cursor position.

Call registers:

AH = 09H
AL = Character to display
BH = Video page, where the character will display it;
BL = Attribute to use
number of repetition.

Return registers:

None

This function displays a character on the computer screen several times, using a specified number in the CX register but without changing the cursor position on the computer screen.

0AH FUNCTION

Use:

Displays a character in the actual cursor position.

Call registers:

AH = 0AH
AL = Character to display
BH = Video page where the character will display it
BL = Color to use (graphic mode only).
CX = number of repetitions

Return registers:

None.

The main difference between this function and the last one is that this one doesn't allow modifications on the attributes neither does it change the cursor position.

0EH FUNCTION

Use:

Displays a character on the computer screen dates the cursor position.

Call registers:

AH = 0EH
AL = Character to display
BH = Video page where the character will display it
BL = Color to use (graphic mode only).

Return registers:

None

5.4.3 16H INTERRUPTION

We will see two functions of the 16 h interruption, these functions are
called by using the AH register.

Functions of the 16h interruption

00H Function, reads a character from the keyboard.
01H Function, reads the keyboard state.

00H FUNCTION USE:

Reads a character from the keyboard.

Call registers:

AH = 00H

Return registers:

AH = Scan code of the keyboard
AL = ASCII value of the character

When we use this interruption, the program executing is halted until a character is typed, if this is an ASCII value; it is stored in the Ah register, Else the scan code is stored in the AL register and the AH register contents the value 00h.

The proposal of the scan code is to use it with the keys without ASCII representation as [ALT][CONTROL], the function keys and so on.

01H FUNCTION

Use:

Reads the keyboard state

Call registers:

AH = 01H

Return registers:

If the flag register is zero, this means, there is information on the buffer memory, else, there is no information in the buffer memory. Therefore the value of the Ah register will be the value key stored in the buffer memory.

5.4.4 17H INTERRUPTION

Purpose: Handles the printer input/output.

Syntax:

Int 17H

This interruption is used to write characters on the printer, sets printer and reads the printer state.

Functions of the 16h interruptions

00H Function, prints value ASCII out
01H Function, sets printer
02H Function, the printer state

00H FUNCTION

Use:

Writes a character on the printer.

Call registers:

 AH = 00H
AL = Character to print.
DX = Port to use.

Return registers:

 AH = Printer device state.

The port to use is in the DX register, the different values are: LPT1 = 0,
LPT2 = 1, LPT3 = 2 ...

The printer device state is coded bit by bit as follows:

BIT 1/0 MEANING
----------------------------------------
0 1 The waited time is over
1 -
2 -
3 1 input/output error
4 1 Chosen printer
5 1 out-of-paper
6 1 communication recognized
7 1 The printer is ready to use

1 and 2 bits are not relevant

Most BIOS sport 3 parallel ports, although there are BIOS which sport 4 parallel ports.

01H FUNCTION

Use:

Sets parallel port.

Call registers:

 AH = 01H
DX = Port to use

Return registers:

 AH = Printer status

Port to use is defined in the DX register, for example: LPT=0, LPT2=1, and so on.

The state of the printer is coded bit by bit as follows:

BIT 1/0 MEANING
----------------------------------------
0 1 The waited time is over
1 -
2 -
3 1 input/output error
4 1 Chosen printer
5 1 out-of-paper
6 1 communication recognized
7 1 The printer is ready to use

1 and 2 bits are not relevant

Most BIOS sport 3 parallel ports, although there are BIOS which sport 4 parallel ports.

02H FUNCTION

Uses:

Gets the printer status.

Call registers:

 AH = 01H
DX = Port to use

Return registers

 AH = Printer status.

Port to use is defined in the DX register, for example: LPT=0, LPT2=1, and
so on

The state of the printer is coded bit by bit as follows:

BIT 1/0 MEANING
----------------------------------------
0 1 The waited time is over
1 -
2 -
3 1 input/output error
4 1 Chosen printer
5 1 out-of-paper
6 1 communication recognized
7 1 The printer is ready to use

1 and 2 bits are not relevant

Most BIOS sport 3 parallel ports, although there are BIOS which sport 4
parallel ports.

5.5 Ways of working with files

There are two ways to work with files, the first one is by means of file control blocks or "FCB" and the second one is by means of communication channels, also known as "handles".

The first way of file handling has been used since the CPM operative system, predecessor of DOS, thus it assures certain compatibility with very old files from the CPM as well as from the 1.0 version of the DOS, besides this method allows us to have an unlimited number of open files at the same time. If you want to create a volume for the disk the only way to achieve this is by using this method.

Even after considering the advantages of the FCB, the use of the communication channels it is much simpler and it allows us a better handling of errors, besides, since it is much newer it is very probable that the files created this way maintain themselves compatible through later versions of the operative system.

For a greater facility on later explanations I will refer to the file control blocks as FCBs and to the communication channels as handles.

5.6 FCB method

Contents

5.6.1 Introduction
5.6.2 Open files
5.6.3 Create a new file
5.6.4 Sequential writing
5.6.5 Sequential reading
5.6.6Random reading and writing
5.6.7 Close a file

5.6.1 Introduction

There are two types of FCB, the normal, whose length is 37 bytes and the extended one of 44 bytes. On this tutorial we will only deal with the first type, so from now on when I refer to an FCB, I am really talking about a 37 bytes FCB.

The FCB is composed of information given by the programmer and by information which it takes directly from the operative system.

When thesetypes of files are used it is only possible to work on the current directory since the FCBs do not provide sport for the use of the organization by directories of DOS.

The FCB is formed by the following fields:

POSITION LENGTH MEANING
00H 1 Byte Drive
01H 8 Bytes File name
09H 3 Bytes Extension
0CH 2 Bytes Block number
0EH 2 Bytes Register size
10H 4 Bytes File size
14H 2 Bytes Creation date
16H 2 Bytes Creation hour
18H 8 Bytes Reserved
20H 1 Bytes Current register
21H 4 Bytes Random register

To select the work drive the next format is followed: drive A = 1; drive B = 2; etc. If 0 is used the drive being used at that moment will be taken as option.

The name of the file must be justified to the left and in case it is necessary the remaining bytes will have to be filled with spaces, and the extension of the file is placed the same way.

The current block and the current register tell the computer which register will be accessed on reading or writing operations. A block is a group of 128 registers. The first block of the file is the block 0. The first register is the register 0, therefore the last register of the first block would be the 127, since the numbering started with 0 and the block can contain 128 registers in total.

5.6.2 Opening files

To open an FCB file the 21H interruption, 0FH function is used. The unit, the name and extension of the file must be initialized before opening it.

The DX register must point to the block. If the value of FFH is returned on the AH register when calling on the interruption then the file was not found, if everything came out well a value of 0 will be returned.

If the file is opened then DOS initializes the current block to 0, the size of the register to 128 bytes and the size of the same and its date are filled with the information found in the directory.

5.6.3 Creating a new file

For the creation of files the 21H interruption 16H function is used. DX must point to a control structure whose requirements are that at least the logic unit, the name and the extension of the file be defined. In case there is a problem the FFH value will be returned on AL, otherwise this register will contain a value of 0.

5.6.4 Sequential writing

Before we can perform writing to the disk it is necessary to define the data transfer area using for this end the 1AH function of the 21H interruption.

The 1AH function does not return any state of the disk nor or the operation, but the 15H function, which is the one we will use to write to the disk, does it on the AL register, if this one is equal to zero there was no error and the fields of the current register and block are dated.

5.6.5 Sequential reading

Before anything we must define the file transfer area or DTA. In order to sequentially read we use the 14H function of the 21H interruption.

The register to be read is the one which is defined by the current block and register. The AL register returns to the state of the operation, if AL contains a value of 1 or 3 it means we have reached the end of the file. A value of 2 means that the FCB is wrongly structured.

In case there is no error, AL will contain the value of 0 and the fields of the current block and register are dated.

5.6.6 Random reading and writing

The 21H function and the 22H function of the 21H interruption are the ones in charge of realizing the random readings and writings respectively.

The random register number and the current block are used to calculate the relative position of the register to read or write.

The AL register returns the same information for the sequential reading of writing. The information to be read will be returned on the transfer area of the disk, likewise the information to be written resides on the DTA.

5.6.7 Closing a file

To close a file we use the 10H function of the 21H interruption.

If after invoking this function, the AL register contains the FFH value, this means that the file has changed position, the disk was changed or there is error of disk access.

5.7 Channels of communication

Contents

5.7.1 Working with handles
5.7.2 Functions to use handles

5.7.1 Working with handles

The use of handles to manage files greatly facilitates the creation of files and programmer can concentrate on other aspects of the programming without worrying on details which can be handled by the operative system.

The easy use of the handles consists in that to operate o a file, it is only necessary to define the name of the same and the number of the handle to use, all the rest of the information is internally handled by the DOS.

When we use this method to work with files, there is no distinction between sequential or random accesses, the file is simply taken as a chain of bytes.

5.7.2 Functions to use handles

The functions used for the handling of files through handles are described in unit 6: Interruptions, in the section dedicated to the 21H interruption.

6 Macros and procedures

Contents

6.1 Procedures
6.2 Macros

6.1 Procedure

Definition of procedure

A procedure is a collection of instructions to which we can direct the flow of our program, and once the execution of these instructions is over control is given back to the next line to process of the code which called on the procedure.

Procedures help us to create legible and easy to modify programs.

At the time of invoking a procedure the address of the next instruction of the program is kept on the stack so that, once the flow of the program has been transferred and the procedure is done, one can return to the next line of the original program, the one which called the procedure.

Syntax of a Procedure

There are two types of procedures, the intrasegments, which are found on the same segment of instructions, and the inter-segments which can be stored on different memory segments.

When the intrasegment procedures are used, the value of IP is stored on the stack and when the intrasegments are used the value of CS:IP is stored.

To divert the flow of a procedure (calling it), the following directive is used:

CALL NameOfTheProcedure

The part which make a procedure are:

Declaration of the procedure
Code of the procedure
Return directive
Termination of the procedure

For example, if we want a routine which adds two bytes stored in AH and AL each one, and keep the addition in the BX register:

Adding Proc Near ; Declaration of the procedure
Mov Bx, 0 ; Content of the procedure
Mov B1, Ah
Mov Ah, 00
Add Bx, Ax
Ret ; Return directive
Add Endp ; End of procedure declaration

On the declaration the first word, Adding, corresponds to the name of out procedure, Proc declares it as such and the word Near indicates to the MASM that the procedure is intrasegment.

The Ret directive loads the IP address stored on the stack to return to the original program, lastly, the Add Endp directive indicates the end of the procedure.

To declare an inter segment procedure we substitute the word Near for the word FAR.

The calling of this procedure is done the following way:

Call Adding

Macros offer a greater flexibility in programming compared to the procedures, nonetheless, these last ones will still be used.

6.2 Macros

Contents

6.2.1 Definition of a macro
6.2.2 Syntax of a macro
6.2.3 Macro libraries

6.2.1 Definition of the macro

A macro is a group of repetitive instructions in a program which are codified only once and can be used as many times as necessary.

The main difference between a macro and a procedure is that in the macro the passage of parameters is possible and in the procedure it is not, this is only applicable for the TASM - there are other programming languages which do allow it. At the moment the macro is executed each parameter is substituted by the name or value specified at the time of the call.

We can say then that a procedure is an extension of a determined program, while the macro is a module with specific functions which can be used by different programs.

Another difference between a macro and a procedure is the way of calling each one, to call a procedure the use of a directive is required, on the other hand the call of macros is done as if it were an assembler instruction.

6.2.2 Syntax of a Macro

The parts which make a macro are:

Declaration of the macro
Code of the macro
Macro termination directive

The declaration of the macro is done the following way:

NameMacro MACRO [parameter1, parameter2...]

Even though we have the functionality of the parameters it is possible to create a macro which does not need them.

The directive for the termination of the macro is: ENDM

An example of a macro, to place the cursor on a determined position on the screen is:

Position MACRO Row, Column
PUSH AX
PUSH BX
PUSH DX
MOV AH, 02H
MOV DH, Row
MOV DL, Column
MOV BH, 0
INT 10H
POP DX
POP BX
POP AX
ENDM

To use a macro it is only necessary to call it by its name, as if it were another assembler instruction, since directives are no longer necessary as in the case of the procedures.

Example:

Position 8, 6

6.2.3 Macro Libraries

One of the facilities that the use of macros offers is the creation of libraries, which are groups of macros which can be included in a program from a different file.

The creation of these libraries is very simple, we only have to write a file with all the macros which will be needed and save it as a text file.

To call these macros it is only necessary to use the following instruction Include NameOfTheFile, on the part of our program where we would normally write the macros, this is, at the beginning of our program, before the declaration of the memory model.

The macros file was saved with the name of MACROS.TXT, the instruction Include would be used the following way:

;Beginning of the program
Include MACROS.TXT
.MODEL SMALL
.DATA
;The data goes here
.CODE
Beginning:
;The code of the program is inserted here
.STACK
;The stack is defined
End beginning
;Our program ends

More debug program examples

In this section we provide you several assembler programs to run in the debug program. You can execute each assembler program using the "t" (trace) command, to see what each instruction does.

First example

-a0100
297D:0100 MOV AX,0006 ; Puts value 0006 at register AX
297D:0103 MOV BX,0004 ;Puts value 0004 at register BX
297D:0106 ADD AX,BX ;Adds BX to AX contents
297D:0108 INT 20 ;Causes end of the Program

The only thing that this program does is to save two values in two registers and add the value of one to the other.

Second example

- a100
0C1B:0100 jmp 125 ; Jumps to direction 125H
0C1B:0102 [Enter]
- e 102 'Hello, How are you ?' 0d 0a '$'
- a125
0C1B:0125 MOV DX,0102 ; Copies string to DX register
0C1B:0128 MOV CX,000F ; Times the string will be displayed
0C1B:012B MOV AH,09 ; Copies 09 value to AH register
0C1B:012D INT 21 ; Displays string
0C1B:012F DEC CX ; Reduces in 1 CX
0C1B:0130 JCXZ 0134 ; If CX is equal to 0 jumps to 0134
0C1B:0132 JMP 012D ; Jumps to direction 012D
0C1B:0134 INT 20 ; Ends the program

This program displays on the screen 15 times a character string.

Third example

-a100
297D:0100 MOV AH,01 ;Function to change the cursor
297D:0102 MOV CX,0007 ;Forms the cursor
297D:0105 INT 10 ;Calls for BIOS
297D:0107 INT 20 ;Ends the program

This program is good for changing the form of the cursor.

Fourth example

-a100
297D:0100 MOV AH,01 ; Funtion 1 (reads keyboard)
297D:0102 INT 21 ; Calls for DOS
297D:0104 CMP AL,0D ; Compares if what is read is a carriage return
297D:0106 JNZ 0100 ; If it is not, reads another character
297D:0108 MOV AH,02 ; Funtion 2 (writes on the screen)
297D:010A MOV DL,AL ; Character to write on AL
297D:010C INT 21 ; Calls for DOS
297D:010E INT 20 ; Ends the program

This program uses DOS 21H interruption. It uses two functions of the same: the first one reads the keyboard (function 1) and the second one writes on the screen. It reads the keyboard characters until it finds a carriage
return.

Fifth example

-a100
297D:0100 MOV AH,02 ; Function 2 (writes on the screen)
297D:0102 MOV CX,0008; Puts value 0008 on register CX
297D:0105 MOV DL,00 ; Puts value 00 on register DL
297D:0107 RCL BL,1 ; Rotates the byte in BL to the left by one bit
; through the carry flag
297D:0109 ADC DL,30 ; Converts flag register to1
297D:010C INT 21 ; Calls for DOS
297D:010E LOOP 0105 ; Jumps if CX > 0 to direction 0105
297D:0110 INT 20 ; Ends the program

This program displays on the screen a binary number through a conditional cycle (LOOP) using byte rotation.

Sixth example

-a100
297D:0100 MOV AH,02 ; Function 2 (writes on the screen)
297D:0102 MOV DL,BL ; Puts BL's value on DL
297D:0104 ADD DL,30 ; Adds value 30 to DL
297D:0107 CMP DL,3A ; Compares 3A value with DL's contents without
; affecting its value only modifying the state of
; the car
297D:010A JL 010F ; jumps if < direction 010f
297D:010C ADD DL,07 ; Adds 07 value on DL
297D:010F INT 21 ; Calls for Dos
297D:0111 INT 20 ; Ends the Program

This program prints a zero value on hex digits

Seventh example

-a100
297D:0100 MOV AH,02 ; Function 2 (writes on the screen)
297D:0102 MOV DL,BL ; Puts BL value on DL
297D:0104 AND DL,0F ; Carries ANDing numbers bit by bit
297D:0107 ADD DL,30 ; Adds 30 to Dl
297D:010A CMP DL,3A ; Compares Dl with 3A
297D:010D JL 0112 ; Jumps if < 0112 direction
297D:010F ADD DL, 07 ; Adds 07 to DL
297D:0112 INT 21 ; Calls for Dos
297D:0114 INT 20 ; Ends the program

This program is used to print two digit hex numbers.

Eight example

-a100
297D:0100 MOV AH,02 ; Function 2 (writes on the screen)
297D:0102 MOV DL,BL ; Puts BL value on DL
297D:0104 MOV CL,04 ; Puts 04 value on CL
297D:0106 SHR DL,CL ; Moves per four bits of your number to the
; rightmost nibble
297D:0108 ADD DL,30 ; Adds 30 to DL
297D:010B CMP L,3A ; Compares Dl with 3A
297D:010E JL 0113 ; Jumps if < 0113 direction
297D:0110 ADD DL,07 ; Adds 07 to DL
297D:0113 INT 21 ; Calls for Dos
297D:0115 INT 20 ; Ends the program

This program works for printing the first of two digit hex numbers

Ninth example

-a100
297D:0100 MOV AH,02 ; Function 2 (writes on the screen)
297D:0102 MOV DL,BL ; Puts BL value on DL
297D:0104 MOV CL,04 ; Puts 04 value on CL
297D:0106 SHR DL,CL ; Moves per four bits of your number to the
;rightmost nibble
297D:0108 ADD DL,30 ; Adds 30 to DL
297D:010B CMP DL,3A ; Compares Dl with 3A
297D:010E JL 0113 ; Jumps if < 0113 direction
297D:0110 ADD DL,07 ; Adds 07 to DL
297D:0113 INT 21 ; Calls for Dos
297D:0115 MOV DL,BL ; Puts Bl value on DL
297D:0117 AND DL,0F ; Carries ANDing numbers bit by bit
297D:011A ADD DL,30 ; Adds 30 to DL
297D:011D CMP DL,3A ; Compares Dl with 3A
297D:0120 JL 0125 ; Jumps if < 125 direction
297D:0122 ADD DL,07 ; Adds 07 to DL
297D:0125 INT 21 ; Calls for Dos
297D:0127 INT 20 ; Ends the Program

This program works for printing the second of two digit hex numbers

Tenth example

-a100
297D:0100 MOV AH,01 ; Function 1 (reads keyboard)
297D:0102 INT 21 ; Calls for Dos
297D:0104 MOV DL,AL ; Puts Al value on DL
297D:0106 SUB DL,30 ; Subtracts 30 from DL
297D:0109 CMP DL,09 ; Compares DL with 09
297D:010C JLE 0111 ; Jumps if <= 0111 direction
297D:010E SUB DL,07 ; Subtracts 07 from DL
297D:0111 MOV CL,04 ; Puts 04 value on CL register
297D:0113 SHL DL,CL ; It inserts zeros to the right
297D:0115 INT 21 ; Calls for Dos
297D:0117 SUB AL,30 ; Subtracts 30 from AL
297D:0119 CMP AL,09 ; Compares AL with 09
297D:011B JLE 011F ; Jumps if <= 011f direction
297D:011D SUB AL,07 ; Subtracts 07 from AL
297D:011F ADD DL,AL ; Adds Al to DL
297D:0121 INT 20 ; Ends the Program

This program can read two digit hex numbers

Eleventh example

-a100
297D:0100 CALL 0200 ; Calls for a procedure
297D:0103 INT 20 ;Ends the program

-a200
297D:0200 PUSH DX ; Puts DX value on the stack
297D:0201 MOV AH,08 ; Function 8
297D:0203 INT 21 ; Calls for Dos
297D:0205 CMP AL,30 ; Compares AL with 30
297D:0207 JB 0203 ; Jumps if CF is activated towards 0203 direction
297D:0209 CMP AL,46 ; Compares AL with 46
297D:020B JA 0203 ; jumps if <> 0203 direction
297D:020D CMP AL,39 ; Compares AL with 39
297D:020F JA 021B ; Jumps if <> 021B direction
297D:0211 MOV AH,02 ; Function 2 (writes on the screen)
297D:0213 MOV DL,AL ; Puts Al value on DL
297D:0215 INT 21 ; Calls for Dos
297D:0217 SUB AL,30 ; Subtracts 30 from AL
297D:0219 POP DX ; Takes DX value out of the stack
297D:021A RET ; Returns control to the main program
297D:021B CMP AL,41 ; Compares AL with 41
297D:021D JB 0203 ; Jumps if CF is activated towards 0203 direction
297D:021F MOV AH,02 ; Function 2 (writes on the screen)
297D:022 MOV DL,AL ; Puts AL value on DL
297D:0223 INT 21 ; Calls for Dos
297D:0225 SUB AL,37 ; Subtracts 37 from AL
297D:0227 POP DX ; Takes DX value out of the stack
297D:0228 RET ; Returns control to the main program

This program keeps reading characters until it receives one that can be
converted to a hex number
More Assembler programs examples( using TASM program)

;name of the program:one.asm
;
.model small
.stack
.code
mov AH,1h ;Selects the 1 D.O.S. function
Int 21h ;reads character and return ASCII code to register AL
mov DL,AL ;moves the ASCII code to register DL
sub DL,30h ;makes the operation minus 30h to convert 0-9 digit number
cmp DL,9h ;compares if digit number it was between 0-9
jle digit1 ;If it true gets the first number digit (4 bits long)
sub DL,7h ;If it false, makes operation minus 7h to convert letter A-F
digit1:
mov CL,4h ;prepares to multiply by 16
shl DL,CL ; multiplies to convert into four bits upper
int 21h ;gets the next character
sub AL,30h ;repeats the conversion operation
cmp AL,9h ;compares the value 9h with the content of register AL
jle digit2 ;If true, gets the second digit number
sub AL,7h ;If no, makes the minus operation 7h
digit2:
add DL,AL ;adds the second number digit
mov AH,4CH
Int 21h ;21h interruption
End ; finishs the program code

This program reads two characters from the keyboard and prints them on the screen.

;name the program:two.asm
.model small
.stack
.code
PRINT_A_J PROC
MOV DL,'A' ;moves the A character to register DL
MOV CX,10 ;moves the decimal value 10 to register cx
;This number value its the time to print out after the A ;character
PRINT_LOOP:
CALL WRITE_CHAR ;Prints A character out
INC DL ;Increases the value of register DL
LOOP PRINT_LOOP ;Loop to print out ten characters
MOV AH,4Ch ;4Ch function of the 21h interruption
INT 21h ;21h interruption
PRINT_A_J ENDP ;Finishes the procedure

WRITE_CHAR PROC
MOV AH,2h ;2h function of the 21 interruption
INT 21h ;Prints character out from the register DL
RET ;Returns the control to procedure called
WRITE_CHAR ENDP ;Finishes the procedure
END PRINT_A_J ;Finishes the program code

This program prints the a character through j character on the screen

;name of the program :three.asm
.model small
.STACK
.code

TEST_WRITE_HEX PROC
MOV DL,3Fh ;moves the value 3Fh to the register DL
CALL WRITE_HEX ;Calls the procedure
MOV AH,4CH ;4Ch function
INT 21h ;Returns the control to operating system
TEST_WRITE_HEX ENDP ;Finishes the procedure

PUBLIC WRITE_HEX
;........................................................;
; This procedure converts into hexadecimal number the byte is in the register DL and show the digit number;
;Use:WRITE_HEX_DIGIT ;
;........................................................;

WRITE_HEX PROC
PUSH CX ;pushes the value of the register CX to the stack memory
PUSH DX ;pushes the value of the register DX to the stack memory
MOV DH,DL ;moves the value of the register DL to register DH
MOV CX,4 ;moves the value numeric 4 to register CX
SHR DL,CL
CALL WRITE_HEX_DIGIT ;shows on the computer screen, the first hexadecimal number
MOV DL,DH ;moves the value of the register DH to the register DL
AND DL,0Fh ;ANDing the upper bit
CALL WRITE_HEX_DIGIT ; shows on the computer screen, the second hexadecimal number
POP DX ;pops the value of the register DX to register DX
POP CX ; pops the value of the register DX to register DX
RET ;Returns the control of the procedure called
WRITE_HEX ENDP

PUBLIC WRITE_HEX_DIGIT
;......................................................................;
; ;
; This procedure converts the lower 4 bits of the register DL into hexadecimal ;number and show them in the computer screen ;
;Use: WRITE_CHAR ;
;......................................................................;

WRITE_HEX_DIGIT PROC
PUSH DX ;Pushes the value of the register DX in the stack memory
CMP DL,10 ;compares if the bit number is minus than number ten
JAE HEX_LETTER ;No , jumps HEX_LETER
ADD DL,"0" ;yes, it converts into digit number
JMP Short WRITE_DIGIT ;writes the character
HEX_LETTER:
ADD DL,"A"-10 ;converts a character into hexadecimal number
WRITE_DIGIT:
CALL WRITE_CHAR ;shows the character in the computer screen
POP DX ;Returns the initial value of the register DX to register DL
RET ;Returns the control of the procedure called
WRITE_HEX_DIGIT ENDP

PUBLIC WRITE_CHAR
;......................................................................;
;This procedure shows the character in the computer screen using the D.O.S. ;
;......................................................................;

WRITE_CHAR PROC
PUSH AX ;pushes the value of the register AX in the stack memory
MOV AH,2 ;2h Function
INT 21h ;21h Interruption
POP AX ;Pops the initial value of the register AX to the register AX
RET ;Returns the control of the procedure called
WRITE_CHAR ENDP

END TEST_WRITE_HEX ;finishes the program code

This program prints a predefined value on the screen

;name of the program:five.asm
.model small
.stack
.code

PRINT_ASCII PROC
MOV DL,00h ;moves the value 00h to register DL
MOV CX,255 ;moves the value decimal number 255. this decimal number
;will be 255 times to print out after the character A
PRINT_LOOP:
CALL WRITE_CHAR ;Prints the characters out
INC DL ;Increases the value of the register DL content
LOOP PRINT_LOOP ;Loop to print out ten characters
MOV AH,4Ch ;4Ch function
INT 21h ;21h Interruption
PRINT_ASCII ENDP ;Finishes the procedure

WRITE_CHAR PROC
MOV AH,2h ;2h function to print character out
INT 21h ;Prints out the character in the register DL
RET ;Returns the control to the procedure called
WRITE_CHAR ENDP ;Finishes the procedure

END PRINT_ASCII ;Finishes the program code

This program prints the 256 ASCII code on the screen

dosseg
.model small
.stack
.code
write proc
mov ah,2h;
mov dl,2ah;
int 21h
mov ah,4ch
int 21h
write endp

end write

This program prints a defined character using an ASCII code on the screen.

.model small; the name of the program is seven.asm
.stack;
.code;

EEL: MOV AH,01 ; 1 function (reads one character from the keyboard)
INT 21h ; 21h interruption
CMP AL,0Dh ; compares the value with 0dh
JNZ EEL ;jumps if no equal of the label eel
MOV AH,2h ; 2 function (prints the character out on the screen)
MOV DL,AL ;moves the value of the register AL to the register DL
INT 21h ;21 interruption
MOV AH,4CH ;4C function (returns the control to the D.O.S. operating system)
INT 21h ;21 interruption

END ;finishes the program

This program reads characters form the keyboard and prints them on the screen until find the return character.


I. Préface

I-A. Objectif

L'objectif de ce livre est de permettre au lecteur de mieux comprendre comment les ordinateurs fonctionnent réellement à un niveau plus bas que les langages de programmation comme Pascal. En ayant une compréhension plus profonde de la façon dont fonctionnent les ordinateurs, le lecteur peu devenir plus productif dans le développement de logiciels dans des langages de plus haut niveau comme le C et le C++.

Apprendre à programmer en assembleur est un excellent moyen d'atteindre ce but. Les autres livres d'assembleur pour PC apprennent toujours à programmer le processeur 8086 qu'utilisait le PC originel de 1981 ! Le processeur 8086 ne supportait que le mode réel.

Dans ce mode, tout programme peu adresser n'importe quel endroit de la mémoire ou n'importe quel périphérique de l'ordinateur. Ce mode n'est pas utilisable pour un système d'exploitation sécurisé et multitâche. Ce livre parle plutôt de la façon de programmer les processeurs 80386 et plus récents en mode protégé (le mode dans lequel fonctionnent Windows et Linux). Ce mode supporte les fonctionnalités que les systèmes d'exploitation modernes offrent, comme la mémoire virtuelle et la protection mémoire. Il y a plusieurs raisons d'utiliser le mode protégé :

1. Il est plus facile de programmer en mode protégé qu'en mode réel 8086 que les autres livres utilisent ;

2. Tous les systèmes d'exploitation PC modernes fonctionnent en mode protégé ;

3. Il y a des logiciels libres disponibles qui fonctionnent dans ce mode.

Le manque de livres sur la programmation en assembleur PC en mode protégé est la principale raison qui a conduit l'auteur à écrire ce livre.

Comme nous y avons fait allusion ci-dessus, ce texte utilise des logiciels Libres/Open Source : l'assembleur NASM et le compilateur C/C++ DJGPP. Les deux sont disponibles en téléchargement sur Internet. Ce texte parle également de l'utilisation de code assembleur NASM sous Linux et avec les compilateurs C/C++ de Borland et Microsoft sous Windows. Les exemples pour toutes ces plateformes sont disponibles sur mon site Web : http://www.drpaulcarter.com/pcasm.

Vous devez télécharger le code exemple si vous voulez assembler et exécuter la plupart des exemples de ce tutoriel.

Soyez conscient que ce texte ne tente pas de couvrir tous les aspects de la programmation assembleur. L'auteur a essayé de couvrir les sujets les plus importants avec lesquels tous les programmeurs devraient être familiers.

I-B. Remerciements

L'auteur voudrait remercier les nombreux programmeurs qui ont contribué au mouvement Libre/Open Source. Tous les programmes et même ce livre lui-même ont été créés avec des logiciels gratuits. L'auteur voudrait remercier en particulier John S. Fine, Simon Tatham, Julian Hall et les autres développeurs de l'assembleur NASM sur lequel tous les exemples de ce livre sont basés ; DJ Delorie pour le développement du compilateur C/C++ DJGPP utilisé ; les nombreuses personnes qui ont contribué au compilateur GNU gcc sur lequel DJGPP est basé ; Donald Knuth et les autres pour avoir développé les langages composition TEX et LATEX2" qui ont été utilisés pour produire le livre ; Richard Stallman (fondateur de la Free Software Foundation), Linus Torvalds (créateur du noyau Linux) et les autres qui ont créé les logiciels sous-jacents utilisés pour ce travail.

Merci aux personnes suivantes pour leurs corrections :

John S. Fine ; Marcelo Henrique Pinto de Almeida ; Sam Hopkins ; Nick D'Imperio ; Jeremiah Lawrence ; Ed Beroset ; Jerry Gembarowski ; Ziqiang Peng ; Eno Compton ; Josh I Cates ; Mik Miin ; Luke Wallis ; Gaku Ueda ; Brian Heward ; Chad Gorshing ; F. Gotti ; Bob Wilkinson ; Markus Koegel ;Louis Taber.

I-C. Ressources sur Internet

Page de l'auteur : http://www.drpaulcarter.com/.

Page NASM sur SourceForge : http://sourceforge.net/projects/nasm/.

DJGPP : http://www.delorie.com/djgpp.

Assembleur Linux : http://www.linuxassembly.org/.

The Art of Assembly : http://webster.cs.ucr.edu/.

USENET comp.lang.asm.x86

Documentation Intel : http://developer.intel.com/design/Pentium4/documentation.htm.

I-D. Réactions

L'auteur accepte toute réaction sur ce travail.

E-mail : pacman128@gmail.com.

WWW : http://www.drpaulcarter.com/pcasm

II. Introduction

II-A. Systèmes numériques

La mémoire d'un ordinateur est constituée de nombres. Cette mémoire ne stocke pas ces nombres en décimal (base 10). Comme cela simplifie grandement le matériel, les ordinateurs stockent toutes les informations au format binaire (base 2). Tout d'abord, revoyons ensemble le système décimal.

II-A-1. Décimal

Les nombres en base 10 sont constitués de 10 chiffres possibles (0-9).

Chaque chiffre d'un nombre est associé à une puissance de 10 selon sa position dans le nombre. Par exemple :

234 = 2 x 102 + 3 × 101 + 4 × 100

II-A-2. Binaire

Les nombres en base 2 sont composés de deux chiffres possibles (0 et 1). Chaque chiffre est associé à une puissance de 2 selon sa position dans le nombre (un chiffre binaire isolé est appelé bit). Par exemple :

110012 = 1 × 24+ 1 × 23 + 0 × 22 + 0 × 21 + 1 × 20

= 16 + 8 + 1

= 25

Cet exemple montre comment passer du binaire au décimal. Le tableau 1.1 montre la représentation binaire des premiers nombres.

La figure 1.1 montre comment des chiffres binaires individuels (i.e., des bits) sont additionnés. Voici un exemple :

Image non disponible

Si l'on considère la division décimale suivante :

1234 ÷ 10 = 123 r 4

on peut voir que cette division sépare le chiffre le plus à droite du nombre et décale les autres chiffres d'une position vers la droite. Diviser par deux effectue une opération similaire, mais pour les chiffres binaires du nombre.

Considérons la division binaire suivante (1) :

11012 ÷ 102 = 1102 r 1

Cette propriété peut être utilisée pour convertir un nombre décimal en son équivalent binaire comme le montre la Figure 1.2. Cette méthode trouve le chiffre binaire le plus à droite en premier, ce chiffre est appelé le bit le moins signicatif (lsb, least signicant bit). Le chiffre le plus à gauche est appelé le bit le plus signicatif (msb, most signicant bit). L'unité de base de la mémoire consiste en un jeu de 8 bits appelé octet (byte).

Image non disponible

II-A-3. Hexadécimal

Les nombres hexadécimaux utilisent la base 16. L'hexadécimal (ou hexa en abrégé) peut être utilisé comme notation pour les nombres binaires. L'hexa a 16 chiffres possibles. Cela pose un problème, car il n'y a pas de symbole à utiliser pour les chiffres supplémentaires après 9. Par convention, on utilise des lettres pour les représenter. Les 16 chiffres de l'hexa sont 0-9 puis A, B, C, D, E et F. Le chiffre A équivaut à 10 en décimal, B à 11, etc. Chaque chiffre d'un nombre en hexa est associé à une puissance de 16. Par exemple :

2BD16 = 2 × 162 + 11 × 161 + 13 × 160

= 512 + 176 + 13

= 701

Pour convertir un nombre hexa en binaire, convertissez simplement chaque chiffre hexa en un nombre binaire de 4 bits. Par exemple, 24D16 est converti en 0010 0100 11012. Notez que les 0 de tête des 4 bits sont importants ! Si le 0 de tête pour le chiffre du milieu de 24D16 n'est pas utilisé, le résultat est faux. La conversion du binaire vers l'hexa est aussi simple. On effectue la même chose, dans l'autre sens. Convertissez chaque segment de 4 bits du binaire vers l'hexa. Commencez à droite, pas à gauche, du nombre binaire.

Cela permet de s'assurer que le procédé utilise les segments de 4 bits corrects (2)

Exemple :

Image non disponible

Un nombre de 4 bits est appelé quadruplet(nibble). Donc, chaque chiffre hexa correspond à un quadruplet. Deux quadruplets forment un octet et donc un octet peut être représenté par un nombre hexa à deux chiffres. La valeur d'un bit va de 0 à 11111111 en binaire, 0 à FF en hexa et 0 à 255 en décimal.

II-B. Organisation de l'ordinateur

II-B-1. Mémoire

La mémoire est mesurée en kilo-octets ( 210= 1024 octets), mega octets (220= 1 048 576 octets) et gigaoctets (230 = 1 073 741 824 octets).

L'unité mémoire de base est l'octet. Un ordinateur avec 32 mégaoctets de mémoire peut stocker jusqu'à environ 32 millions d'octets d'informations.

Chaque octet en mémoire est étiqueté par un nombre unique appelé son adresse comme le montre la Figure 1.4.

Image non disponible

Souvent, la mémoire est utilisée par bouts plus grands que des octets isolés. Sur l'architecture PC, on a donné des noms à ces portions de mémoire plus grandes, comme le montre le Tableau 1.2.

Image non disponible

Toutes les données en mémoire sont numériques. Les caractères sont stockés en utilisant un code caractère qui fait correspondre des nombres aux caractères. Un des codes caractère les plus connus est appelé ASCII (American Standard Code for Information Interchange, Code Américain Standard pour l'Échange d'Informations). Un nouveau code, plus complet, qui supplante l'ASCII est l'Unicode. Une des différences-clés entre les deux codes est que l'ASCII utilise un octet pour encoder un caractère alors que l'Unicode en utilise deux (ou un mot). Par exemple, l'ASCII fait correspondre l'octet 4116 (6510) au caractère majuscule A ; l'Unicode y fait correspondre le mot 004116. Comme l'ASCII n'utilise qu'un octet, il est limité à 256 caractères différents au maximum (3). L'Unicode étend les valeurs ASCII à des mots et permet de représenter beaucoup plus de caractères. C'est important afin de représenter les caractères de tous les langages du monde.

II-B-2. Le CPU (processeur)

Le processeur (CPU, Central Processing Unit) est le dispositif physique qui exécute les instructions. Les instructions que les processeurs peuvent exécuter sont généralement très simples. Elles peuvent nécessiter que les données sur lesquelles elles agissent soient présentes dans des emplacements de stockage spéciques dans le processeur lui-même appelés registres. Le processeur peut accéder aux données dans les registres plus rapidement qu'aux données en mémoire. Cependant, le nombre de registres d'un processeur est limité, donc le programmeur doit faire attention à n'y conserver que les données actuellement utilisées.

Les instructions que peut exécuter un type de processeur constituent le langage machine. Les programmes machine ont une structure beaucoup plus basique que les langages de plus haut niveau. Les instructions du langage machine sont encodées en nombres bruts, pas en format texte lisible. Un processeur doit être capable de décoder les instructions très rapidement pour fonctionner efficacement. Le langage machine est conçu avec ce but en tête, pas pour être facilement déchiffrable par les humains. Les programmes écrits dans d'autres langages doivent être convertis dans le langage machine programme qui traduit les programmes écrits dans un langage de programmation en langage machine d'une architecture d'ordinateur particulière. En général, chaque type de processeur a son propre langage machine unique. C'est une des raisons pour lesquelles un programme écrit pour Mac ne peut pas être exécuté sur un PC.

Les ordinateurs utilisent une horloge pour synchroniser l'exécution des instructions. L'horloge tourne à une fréquence fixée (appelée vitesse d'horloge). Lorsque vous achetez un ordinateur à 1,5GHz, 1,5GHz est la fréquence de cette horloge.

GHz signifie gigahertz ou un milliard de cycles par seconde. Un processeur à 1,5GHz a 1,5 milliard d'impulsions horloge par seconde.

L'horloge ne décompte pas les minutes et les secondes. Elle bat simplement à un rythme constant. Les composants électroniques du processeur utilisent les pulsations pour effectuer leurs opérations correctement, comme le battement d'un métronome aide à jouer de la musique à un rythme correct. Le nombre de battements (ou, comme on les appelle couramment cycles) que requiert une instruction dépend du modèle et de la génération du processeur. Le nombre de cycles dépend de l'instruction.

II-B-3. La famille des processeurs 80x86

Les PC contiennent un processeur de la famille des Intel 80x86 (ou un clone). Les processeurs de cette famille ont tous des fonctionnalités en commun, y compris un langage machine de base. Cependant, les membres les plus récents améliorent grandement les fonctionnalités.

8088,8086 : ces processeurs, du point de vue de la programmation sont identiques. Ils étaient les processeurs utilisés dans les tout premiers PC. Ils offrent plusieurs registres 16 bits : AX, BX, CX, DX, SI, DI, BP, SP, CS, DS, SS, ES, IP, FLAGS. Ils ne supportent que jusqu'à 1Mo de mémoire et n'opèrent qu'en mode réel. Dans ce mode, un programme peut accéder à n'importe quelle adresse mémoire, même la mémoire des autres programmes ! Cela rend le débogage et la sécurité très difficiles ! De plus, la mémoire du programme doit être divisée en segments. Chaque segment ne peut pas dépasser les 64 Ko.

80286 : ce processeur était utilisé dans les PC de type AT. Il apporte quelques nouvelles instructions au langage machine de base des 8088/86. Cependant, sa principale nouvelle fonctionnalité est le mode protégé 16 bits. Dans ce mode, il peut accéder jusqu'à 16 Mo de mémoire et empêcher les programmes d'accéder à la mémoire des uns et des autres. Cependant, les programmes sont toujours divisés en segments qui ne peuvent pas dépasser les 64 Ko.

80386 : ce processeur a grandement amélioré le 80286. Tout d'abord, il étend la plupart des registres à 32 bits (EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP, EIP) et ajoute deux nouveaux registres 16 bits : FS et GS.

Image non disponible

Il ajoute également un nouveau mode protégé 32 bits. Dans ce mode, il peut accéder jusqu'à 4 Go de mémoire. Les programmes sont encore divisés en segments, mais maintenant chaque segment peut également faire jusqu'à 4 Go !

80486/Pentium/Pentium Pro : ces membres de la famille 80x86 apportent très peu de nouvelles fonctionnalités. Ils accélèrent principalement l'exécution des instructions.

Pentium MMX : ce processeur ajoute les instructions MMX (Multi Media eXentions) au Pentium. Ces instructions peuvent accélérer des opérations graphiques courantes.

Pentium II : c'est un processeur Pentium Pro avec les instructions MMX (le Pentium III est grossièrement un Pentium II plus rapide).

II-B-4. Regitres 16 bits du 8086

Le processeur 8086 original fournissait quatre registres généraux de 16 bits : AX, BX, CX et DX. Chacun de ces registres peut être décomposé en deux registres de 8 bits. Par exemple, le registre AX pouvait être décomposé en AH et AL comme le montre la Figure 1.5. Le registre AH contient les 8 bits de poids fort de AX et AL contient les 8 bits de poids faible. Souvent, AH et AL sont utilisés comme des registres d'un octet indépendants ; cependant, il est important de réaliser qu'ils ne sont pas indépendants de AX. Changer la valeur de AX changera les valeurs de AL et BL et vice versa. Les registres généraux sont utilisés dans beaucoup de déplacements de données et instructions arithmétiques.

Il y a deux registres d'index de 16 bits : SI et DI. Ils sont souvent utilisés comme des pointeurs, mais peuvent être utilisés pour la plupart des mêmes choses que les registres généraux. Cependant, ils ne peuvent pas être décomposés en registres de 8 bits.

Les registres 16 bits BP et SP sont utilisés pour pointer sur des données dans la pile du langage machine et sont appelés le pointeur de base et le pointeur de pile, respectivement. Nous en reparlerons plus tard.

Les registres 16 bits CS, DS, SS et ES sont des registres de segment. Ils indiquent quelle zone de la mémoire est utilisée pour les différentes parties d'un programme. CS signifie Code Segment, DS Data Segment, SS Stack Segment (segment de pile) et ES Extra Segment. ES est utilisé en tant que registre de segment temporaire. Des détails sur ces registres se trouvent dans les sections II.B.6 et II.B.7.

Le registre de pointeur d'instruction (IP) est utilisé avec le registre CS pour mémoriser l'adresse de la prochaine instruction à exécuter par le processeur.

Normalement, lorsqu'une instruction est exécutée, IP est incrémenté pour pointer vers la prochaine instruction en mémoire.

Le registre FLAGS stocke des informations importantes sur les résultats d'une instruction précédente. Ces résultats sont stockés comme des bits individuels dans le registre. Par exemple, le bit Z est positionné à 1 si le résultat de l'instruction précédente était 0 ou à 0 sinon. Toutes les instructions ne modifient pas les bits dans FLAGS, consultez le tableau dans l'appendice pour voir comment chaque instruction affecte le registre FLAGS.

II-B-5. Registres 32 bits du 80386

Les processeurs 80386 et plus récents ont des registres étendus. Par exemple le registre AX 16 bits est étendu à 32 bits. Pour la compatibilité ascendante, AX fait toujours référence au registre 16 bits et on utilise EAX pour faire référence au registre 32 bits. AX représente les 16 bits de poids faible de EAX tout comme AL représente les 8 bits de poids faible de AX (et de EAX). Il n'y a pas moyen d'accéder aux 16 bits de poids fort de EAX directement. Les autres registres étendus sont EBX, ECX, EDX, ESI et EDI.

La plupart des autres registres sont également étendus. BP devient EBP ; SP devient ESP ; FLAGS devient EFLAGS et IP devient EIP. Cependant, contrairement aux registres généraux et d'index, en mode protégé 32 bits (dont nous parlons plus loin) seules les versions étendues de ces registres sont utilisées.

Les registres de segment sont toujours sur 16 bits dans le 80386. Il y a également deux registres de segment supplémentaires : FS et GS. Leurs noms n'ont pas de signification particulière. Ce sont des segments temporaires supplémentaires (comme ES).

Une des définitions du terme mot se réfère à la taille des registres de données du processeur. Pour la famille du 80x86, le terme est désormais un peu confus. Dans le Tableau 1.2, on voit que le terme mot est déni comme faisant 2 octets (ou 16 bits). Cette signification lui a été attribuée lorsque le premier 8086 est apparu. Lorsque le 80386 a été développé, il a été décidé de laisser la définition de mot inchangée, même si la taille des registres avait changé.

II-B-6. Mode réel

En mode réel, la mémoire est limitée à seulement un mégaoctet (220 octets).

Alors d'où vient l'infâme limite des 640 Ko du DOS ? Le BIOS requiert une partie des 1 Mo pour son propre code et pour les périphériques matériels comme l'écran.

Les adresses valides vont de 00000 à FFFFF (en hexa). Ces adresses nécessitent un nombre sur 20 bits. Cependant, un nombre de 20 bits ne tiendrait dans aucun des registres 16 bits du 8086. Intel a résolu le problème, en utilisant deux valeurs de 16 bits pour déterminer une adresse. La première valeur de 16 bits est appelée le sélecteur. Les valeurs du sélecteur doivent être stockées dans des registres de segment. La seconde valeur de 16 bits est appelée le déplacement (offset). L'adresse physique identifiée par un couple sélecteur/déplacement 32 bits est calculée par la formule

16 * sélecteur + déplacement

Multiplier par 16 en hexa est facile, il suffit d'ajouter un 0 à la droite du nombre. Par exemple, l'adresse physique référencée par 047C:0048 est obtenue de la façon suivante :

047C0

+0048

04808

De fait, la valeur du sélecteur est un numéro de paragraphe (voir Tableau 1.2).

Les adresses réelles segmentées ont des inconvénients :

  • une seule valeur de sélecteur peut seulement référencer 64Ko de mémoire (la limite supérieure d'un déplacement de 16 bits). Que se passe-t-il si un programme a plus de 64 Ko de code ? Une seule valeur de CS ne peut pas être utilisée pour toute l'exécution du programme. Le programme doit être divisé en sections (appelées segments) de moins de 64 Ko. Lorsque l'exécution passe d'un segment à l'autre, la valeur de CS doit être changée. Des problèmes similaires surviennent avec de grandes quantités de données et le registre DS. Cela peut être très gênant ! ;
  • chaque octet en mémoire n'a pas une adresse segmentée unique. L'adresse physique 04808 peut être référencée par 047C:0048, 047D:0038, 047E:0028 ou 047B:0058. Cela complique la comparaison d'adresses segmentées.

II-B-7. Mode protégé 16 bits

Dans le mode protégé 16 bits du 80286, les valeurs du sélecteur sont interprétées de façon totalement différente par rapport au mode réel. En mode réel, la valeur d'un sélecteur est un numéro de paragraphe en mémoire.

En mode protégé, un sélecteur est un indice dans un tableau de descripteurs. Dans les deux modes, les programmes sont divisés en segments. En mode réel, ces segments sont à des positions fixes en mémoire et le sélecteur indique le numéro de paragraphe auquel commence le segment. En mode protégé, les segments ne sont pas à des positions fixes en mémoire physique. De fait, ils n'ont même pas besoin d'être en mémoire du tout !

Le mode protégé utilise une technique appelée mémoire virtuelle. L'idée de base d'un système de mémoire virtuelle est de ne garder en mémoire que les programmes et les données actuellement utilisés. Le reste des données et du code sont stockés temporairement sur le disque jusqu'à ce qu'on ait à nouveau besoin d'eux. Dans le mode protégé 16 bits, les segments sont déplacés entre la mémoire et le disque selon les besoins. Lorsqu'un segment est rechargé en mémoire depuis le disque, il est très probable qu'il sera à un endroit en mémoire différent de celui où il était avant d'être placé sur le disque.

Tout ceci est effectué de façon transparente par le système d'exploitation. Le programme n'a pas à être écrit différemment pour que la mémoire virtuelle fonctionne.

En mode protégé, chaque segment est assigné à une entrée dans un tableau de descripteurs. Cette entrée contient toutes les informations dont le système a besoin à propos du segment. Ces informations indiquent : s'il est actuellement en mémoire ; s'il est en mémoire, où il se trouve ; les droits d'accès (p.e., lecture seule). L'indice de l'entrée du segment est la valeur du sélecteur stockée dans les registres de segment.

Un journaliste PC bien connu a baptisé le processeur 286 « cerveau mort » (brain dead).

Un gros inconvénient du mode protégé 16 bits est que les déplacements sont toujours des quantités sur 16 bits. En conséquence, les tailles de segment sont toujours limitées au plus à 64 Ko. Cela rend l'utilisation de grands tableaux problématique.

II-B-8. Mode protégé 32 bits

Le 80386 a introduit le mode protégé 32 bits. Il y a deux différences majeures entre les modes protégés 32 bits du 386 et 16 bits du 286 :

1. Les déplacements sont étendus à 32 bits. Cela permet à un déplacement d'aller jusqu'à 4 milliards. Ainsi, les segments peuvent avoir des tailles jusqu'à 4 Go ;

2. Les segments peuvent être divisés en unités plus petites de 4 Ko appelées pages. Le système de mémoire virtuelle fonctionne maintenant avec des pages plutôt qu'avec des segments. Cela implique que seules certaines parties d'un segment peuvent être présentes en mémoire à un instant donné. En mode 16 bits du 286, soit le segment en entier est en mémoire, soit rien n'y est. Ce qui n'aurait pas été pratique avec les segments plus grands que permet le mode 32 bits.

Dans Windows 3.x, le mode standard fait référence au mode protégé 16 bits du 286 et le mode amélioré (enhanced) fait référence au mode 32 bits.

Windows 9X, Windows NT/2000/XP, OS/2 et Linux fonctionnent tous en mode protégé 32 bits paginé.

II-B-9. Interruptions

Quelques fois, le flot ordinaire d'un programme doit être interrompu pour traiter des évènements qui requièrent une réponse rapide. Le matériel d'un ordinateur offre un mécanisme appelé interruptions pour gérer ces évènements.

Par exemple, lorsqu'une souris est déplacée, la souris interrompt le programme en cours pour gérer le déplacement de la souris (pour déplacer le curseur, etc.). Les interruptions provoquent le passage du contrôle à un gestionnaire d'interruptions. Les gestionnaires d'interruptions sont des routines qui traitent une interruption. Chaque type d'interruption est assignée à un nombre entier. Au début de la mémoire physique, réside un tableau de vecteurs d'interruptions qui contient les adresses segmentées des gestionnaires d'interruptions. Le numéro d'une interruption est essentiellement un indice dans ce tableau.

Les interruptions externes proviennent de l'extérieur du processeur (la souris est un exemple de ce type). Beaucoup de périphériques d'E/S soulèvent des interruptions (p.e., le clavier, le timer, les lecteurs de disque, le CD-ROM et les cartes son). Les interruptions internes sont soulevées depuis le processeur, à cause d'une erreur ou d'une instruction d'interruption.

Les interruptions erreur sont également appelées traps. Les interruptions générées par l'instruction d'interruption sont également appelées interruptions logicielles. Le DOS utilise ce type d'interruption pour implémenter son API (Application Programming Interface). Les systèmes d'exploitation plus récents (comme Windows et Unix) utilisent une interface basée sur C (4).

Beaucoup de gestionnaires d'interruptions redonnent le contrôle au programme interrompu lorsqu'ils se terminent. Ils restaurent tous les registres aux valeurs qu'ils avaient avant l'interruption. Ainsi, le programme interrompu s'exécute comme si rien n'était arrivé (excepté qu'il perd quelques cycles processeur). Les traps ne reviennent généralement jamais. Souvent, elles arrêtent le programme.

II-C. Langage assembleur

II-C-1. Langage machine

Chaque type de processeur comprend son propre langage machine. Les instructions dans le langage machine sont des nombres stockés sous la forme d'octets en mémoire. Chaque instruction a son propre code numérique unique appelé code d'opération ou opcode (operation code) en raccourci. Les instructions des processeurs 80x86 varient en taille. L'opcode est toujours au début de l'instruction. Beaucoup d'instructions comprennent également les données (p.e. des constantes ou des adresses) utilisées par l'instruction.

Le langage machine est très difficile à programmer directement. Déchiffrer la signification d'instructions codées numériquement est fatigant pour des humains. Par exemple, l'instruction qui dit d'ajouter les registres EAX et EBX et de stocker le résultat dans EAX est encodée par les codes hexadécimaux suivants :

03 C3

C'est très peu clair. Heureusement, un programme appelé un assembleur peut faire ce travail laborieux à la place du programmeur.

II-C-2. Langage d'assembleur

Un programme en langage d'assembleur est stocké sous la forme de texte (comme un programme dans un langage de plus haut niveau). Chaque instruction assembleur représente exactement une instruction machine. Par exemple, l'instruction d'addition décrite ci-dessus serait représentée en langage assembleur comme suit :

add eax, ebx

Ici, la signification de l'instruction est beaucoup plus claire qu'en code machine.

Le mot add est un mnémonique pour l'instruction d'addition. La forme générale d'une instruction assembleur est :

mnémonique opérande(s)

Un assembleur est un programme qui lit un fichier texte avec des instructions assembleur et convertit l'assembleur en code machine. Les compilateurs sont des programmes qui font des conversions similaires pour les langages de programmation de haut niveau. Un assembleur est beaucoup plus simple qu'un compilateur.

Cela a pris plusieurs années aux scientifiques de l'informatique pour concevoir le simple fait d'écrire un compilateur !

Chaque instruction du langage d'assembleur représente directement une instruction machine. Les instructions d'un langage de plus haut niveau sont beaucoup plus complexes et peuvent requérir beaucoup d'instructions machine.

Une autre différence importante entre l'assembleur et les langages de haut niveau est que comme chaque type de processeur a son propre langage machine, il a également son propre langage d'assemblage. Porter des programmes assembleur entre différentes architectures d'ordinateur est beaucoup plus difficile qu'avec un langage de haut niveau.

Les exemples de ce livre utilisent le Netwide Assembler ou NASM en

raccourci. Il est disponible gratuitement sur Internet (voyez la préface pour l'URL). Des assembleurs plus courants sont l'Assembleur de Microsoft (MASM) ou l'Assembleur de Borland (TASM). Il y a quelques différences de syntaxe entre MASM/TASM et NASM.

II-C-3. Opérandes d'instruction

Les instructions en code machine ont un nombre et un type variables d'opérandes ; cependant, en général, chaque instruction a un nombre fixé d'opérandes (0 à 3). Les opérandes peuvent avoir les types suivants :

registre : ces opérandes font directement référence au contenu des registres du processeur ;

mémoire : ils font référence aux données en mémoire. L'adresse de la donnée peut être une constante codée en dur dans l'instruction ou calculée en utilisant les valeurs des registres. Les adresses sont toujours des déplacements relatifs au début d'un segment ;

immédiat : ce sont des valeurs fixes qui sont listées dans l'instruction elle-même. Elles sont stockées dans l'instruction (dans le segment de code), pas dans le segment de données ;

implicite : ces opérandes ne sont pas entrés explicitement. Par exemple, l'instruction d'incrémentation ajoute 1 à un registre ou à la mémoire. Le 1 est implicite.

II-C-4. Instructions de base

L'instruction la plus basique est l'instruction MOV. Elle déplace les données d'un endroit à un autre (comme l'opérateur d'assignement dans un langage de haut niveau). Elle prend deux opérandes :

Sélectionnez

mov dest, src

La donnée spécifiée par src est copiée vers dest. Une restriction est que les deux opérandes ne peuvent pas être tous deux des opérandes mémoire.

Cela nous montre un autre caprice de l'assembleur. Il y a souvent des règles quelque peu arbitraires sur la façon dont les différentes instructions sont utilisées. Les opérandes doivent également avoir la même taille. La valeur de AX ne peut pas être stockée dans BL.

Voici un exemple (les points-virgules marquent un commentaire) :

Sélectionnez

mov eax, 3 ; stocke 3 dans le registre EAX (3 est un operande immediat)

mov bx, ax ; stocke la valeur de AX dans le registre BX

L'instruction ADD est utilisée pour additionner des entiers.

Sélectionnez

add eax, 4 ; eax = eax + 4

add al, ah ; al = al + ah

L'instruction SUB soustrait des entiers.

Sélectionnez

sub bx, 10 ; bx = bx - 10

sub ebx, edi ; ebx = ebx - edi

Les instructions INC et DEC incrémentent ou décrémentent les valeurs de 1. Comme le 1 est un opérande implicite, le code machine pour INC et DEC est plus petit que celui des instructions ADD et SUB équivalentes.

Sélectionnez

inc ecx ; ecx++

dec dl ; dl--

II-C-5. Directives

Une directive est destinée à l'assembleur, pas au processeur. Les directives sont généralement utilisées pour indiquer à l'assembleur de faire quelque chose ou pour l'informer de quelque chose. Elles ne sont pas traduites en code machine. Les utilisations courantes des directives sont :

  • la définition de constantes ;
  • la définition de mémoire pour stocker des données ;
  • grouper la mémoire en segment ;
  • inclure des codes source de façon conditionnelle ;
  • inclure d'autres fichiers.

Le code NASM est analysé par un préprocesseur, exactement comme en C. Il y a beaucoup de commandes identiques à celles du préprocesseur C.

Cependant, les directives du préprocesseur NASM commencent par un % au lieu d'un # comme en C.

II-C-5-a. La directive equ

La directive equ peut être utilisée pour définir un symbole. Les symboles sont des constantes nommées qui peuvent être utilisées dans le programme assembleur. Le format est le suivant :

Sélectionnez

symbole equ valeur

Les valeurs des symboles ne peuvent pas être redéfinies plus tard.

II-C-5-b. La directive ?ne

Cette directive est semblable à la directive #define du C. Elle est le plus souvent utilisée pour définir des macros, exactement comme en C.

Sélectionnez

?fine SIZE 100
mov eax, SIZE

Image non disponible

Le code ci-dessus définit une macro appelée SIZE et montre son utilisation dans une instruction MOV. Les macros sont plus flexibles que les symboles de deux façons. Elles peuvent être redéfinies et peuvent être plus que de simples nombres constants.

II-C-5-c. Directives de données

Les directives de données sont utilisées dans les segments de données pour réserver de la place en mémoire. Il y a deux façons de réserver de la mémoire. La première ne fait qu'allouer la place pour les données ; a seconde alloue la place et donne une valeur initiale. La première méthode utilise une des directives RESX. Le X est remplacé par une lettre qui détermine la taille de l'objet (ou des objets) qui sera stocké. Le Tableau 1.3 montre les valeurs possibles.

La seconde méthode (qui définit une valeur initiale) utilise une des directives DX. Les lettres X sont les mêmes que celles de la directive RESX.

Il est très courant de marquer les emplacements mémoire avec des labels (étiquettes). Les labels permettent de faire référence facilement aux emplacements mémoire dans le code. Voici quelques exemples :

Sélectionnez

L1 db 0 ; octet libelle L1 avec une valeur initiale de 0
L2 dw 1000 ; mot labelle L2 avec une valeur initiale de 1000
L3 db 110101b ; octet initialise a la valeur binaire 110101 (53 en decimal)
L4 db 12h ; octet initialise a la valeur hexa 12 (18 en decimal)
L5 db 17o ; octet initialise a la valeur octale 17 (15 en decimal)
L6 dd 1A92h ; double mot initialise a la valeur hexa 1A92
L7 resb 1 ; 1 octet non initialise
L8 db "A" ; octet initialise avec le code ASCII du A (65)

Les doubles et simples quotes sont traitées de la même façon. Les définitions de données consécutives sont stockées séquentiellement en mémoire. C'est-à-dire que le mot L2 est stocké immédiatement après L1 en mémoire. Des séquences de mémoire peuvent également être définies.

Sélectionnez

L9 db 0, 1, 2, 3 ; definit 4 octets
L10 db "w", "o", "r", 'd', 0 ; definit une chaine C = "word"
L11 db 'word', 0 ; idem L10

La directive DD peut être utilisée pour définir à la fois des entiers et des constantes à virgule flottante en simple précision (5). Cependant, la directive DQ ne peut être utilisée que pour définir des constantes à virgule flottante en double précision.

Pour les grandes séquences, la directive de NASM TIMES est souvent utile. Cette directive répète son opérande un certain nombre de fois. Par exemple :

Sélectionnez

L12 times 100 db 0 ; equivalent a 100 (db 0)
L13 resw 100 ; reserve de la place pour 100 mots

Souvenez-vous que les labels peuvent être utilisés pour faire référence à des données dans le code. Il y a deux façons d'utiliser les labels. Si un label simple est utilisé, il fait référence à l'adresse (ou offset) de la donnée.

Si le label est placé entre crochets ([ ]), il est interprété comme la donnée à cette adresse. En d'autres termes, il faut considérer un label comme un pointeur vers la donnée et les crochets déréférencent le pointeur, exactement comme l'astérisque en C (MASM/TASM utilisent une convention différente).

En mode 32 bits, les adresses sont sur 32 bits. Voici quelques exemples :

Sélectionnez

1.           mov al, [L1] ; Copie l'octet situe en L1 dans AL
2.           mov eax, L1 ; EAX = addresse de l'octet en L1
3.           mov [L1], ah ; copie AH dans l'octet en L1
4.            mov eax, [L6] ; copie le double mot en L6 dans EAX
5.            add eax, [L6] ; EAX = EAX + double mot en L6
6.           add [L6], eax ; double mot en L6 += EAX
7.           mov al, [L6] ; copie le premier octet du double mot en L6 dans AL

La ligne 7 de cet exemple montre une propriété importante de NASM. L'assembleur ne garde pas de trace du type de données auquel se réfère le label. C'est au programmeur de s'assurer qu'il (ou elle) utilise un label correctement.

Plus tard, il sera courant de stocker des adresses dans les registres et utiliser les registres comme un pointeur en C. Là encore, aucune vérification n'est faite pour savoir si le pointeur est utilisé correctement. À ce niveau, l'assembleur est beaucoup plus sujet à erreur que le C.

Considérons l'instruction suivante :

Sélectionnez

1.           mov [L6], 1 ; stocke 1 en L6

Cette instruction produit une erreur operation size not specified. Pourquoi ?

Parce que l'assembleur ne sait pas s'il doit considérer 1 comme un octet, un mot ou un double mot. Pour réparer cela, il faut ajouter un spécificateur de taille :

Sélectionnez

1.           mov dword [L6], 1 ; stocke 1 en L6

Cela indique à l'assembleur de stocker un 1 dans le double mot qui commence en L6. Les autres spécificateurs de taille sont : BYTE, WORD, QWORD et TWORD (6).

II-C-6. Entrées et sorties

Les entrées et sorties sont des activités très dépendantes du système. Cela implique un interfaçage avec le matériel. Les langages de plus haut niveau, comme C, fournissent des bibliothèques standard de routines pour une interface de programmation simple, uniforme pour les E/S. Les langages d'assembleur n'ont pas de bibliothèque standard. Ils doivent soit accéder directement au matériel (avec une opération privilégiée en mode protégé) ou utiliser les routines de bas niveau éventuellement fournies par le système d'exploitation.

Il est très courant pour les routines assembleur d'être interfacées avec du C. Un des avantages de cela est que le code assembleur peut utiliser les routines d'E/S de la bibliothèque standard du C. Cependant, il faut connaître les règles de passage des informations aux routines que le C utilise.

Ces règles sont trop compliquées pour en parler ici (nous en parlerons plus tard !). Pour simplifier les E/S, l'auteur a développé ses propres routines qui masquent les règles complexes du C et fournissent une interface beaucoup plus simple. Le Tableau 1.4 décrit les routines fournies. Toutes les routines préservent les valeurs de tous les registres, excepté les routines read. Ces routines modifient la valeur du registre EAX. Pour utiliser ces routines, il faut inclure un fichier contenant les informations dont l'assembleur a besoin pour les utiliser. Pour inclure un fichier dans NASM, utilisez la directive du préprocesseur %include. La ligne suivante inclut le fichier requis par les routines d'E/S de l'auteur (7) :

Sélectionnez

1.           %include "asm_io.inc"

Pour utiliser une de ces routines d'affichage, il faut charger EAX avec la valeur correcte et utiliser une instruction CALL pour l'invoquer. L'instruction CALL est équivalente à un appel de fonction dans un langage de haut niveau.

Elle saute à une autre portion de code, mais revient à son origine une fois la routine terminée. Le programme d'exemple ci-dessous montre plusieurs exemples d'appel à ces routines d'E/S.

Image non disponible

II-C-7. Débogage

La bibliothèque de l'auteur contient également quelques routines utiles pour déboguer les programmes. Ces routines de débogage affichent des informations sur l'état de l'ordinateur sans le modifier. Ces routines sont en fait des macros qui sauvegardent l'état courant du processeur puis font appel à une sous-routine. Les macros sont définies dans le fichier asm_io.inc dont nous avons parlé plus haut. Les macros sont utilisées comme des instructions ordinaires. Les opérandes des macros sont séparés par des virgules.

Il y a quatre routines de débogage nommées dump_regs, dump_mem, dump_stack et dump_math ; elles affichent respectivement les valeurs des registres, de la mémoire, de la pile et du coprocesseur arithmétique.

dump_regs : cette macro affiche les valeurs des registres (en hexadécimal) de l'ordinateur sur stdout (i.e. l'écran). Elle affiche également les bits positionnés dans le registre FLAGS (8). Par exemple, si le drapeau zéro (zero ag) est à 1, ZF est affiché. S'il est à 0, il n'est pas affiché. Elle prend en argument un entier qui est affiché également. Cela peut aider à distinguer la sortie de différentes commandes dump_regs.

dump_mem : cette macro affiche les valeurs d'une région de la mémoire (en hexadécimal et également en caractères ASCII). Elle prend trois arguments séparés par des virgules. Le premier est un entier utilisé pour étiqueter la sortie (comme l'argument de dump_regs). Le second argument est l'adresse à afficher (cela peut être un label). Le dernier argument est le nombre de paragraphes de 16 octets à afficher après l'adresse. La mémoire affichée commencera au premier multiple de paragraphe avant l'adresse demandée.

dump_stack : cette macro affiche les valeurs de la pile du processeur (la pile sera présentée dans le Chapitre V). La pile est organisée en doubles-mots et cette routine les affiche de cette façon. Elle prend trois arguments délimités par des virgules. Le premier est un entier (comme pour

dump_regs). Le second est le nombre de doubles-mots à afficher après l'adresse que le registre EBP contient et le troisième argument est le nombre de doubles-mots à afficher avant l'adresse contenue dans EBP.

dump_math : cette macro affiche les valeurs des registres du coprocesseur arithmétique. Elle ne prend qu'un entier en argument qui est utilisé pour étiqueter la sortie comme le fait l'argument de dump_regs.

II-D. Créer un programme

Aujourd'hui, il est très peu courant de créer un programme autonome écrit complètement en langage assembleur. L'assembleur est habituellement utilisé pour optimiser certaines routines critiques. Pourquoi ? Il est beaucoup plus simple de programmer dans un langage de plus haut niveau qu'en assembleur.

De plus, utiliser l'assembleur rend le programme très dur à porter sur d'autres plateformes. En fait, il est rare d'utiliser l'assembleur tout court.

Alors, pourquoi apprendre l'assembleur ?

1. Quelques fois, le code écrit en assembleur peut être plus rapide et plus compact que le code généré par un compilateur.

2. L'assembleur permet l'accès à des fonctionnalités matérielles du système directement qu'il pourrait être difficile ou impossible à utiliser depuis un langage de plus haut niveau.

3. Apprendre à programmer en assembleur aide à acquérir une compréhension plus profonde de la façon dont fonctionne un ordinateur.

4. Apprendre à programmer en assembleur aide à mieux comprendre comment les compilateurs et les langages de haut niveau comme C fonctionnent.

Ces deux derniers points démontrent qu'apprendre l'assembleur peut être utile même si on ne programme jamais dans ce langage plus tard. En fait, l'auteur programme rarement en assembleur, mais il utilise les leçons qu'il en a tirées tous les jours.

II-D-1. Premier programme

Les programmes qui suivront dans ce texte partiront tous du programme de lancement en C de la Figure 1.6.

Image non disponible

Il appelle simplement une autre fonction nommée asm_main. C'est la routine qui sera écrite en assembler proprement dit. Il y a plusieurs avantages à utiliser un programme de lancement en C. Tout d'abord, cela laisse le système du C initialiser le programme de façon à fonctionner correctement en mode protégé. Tous les segments et les registres correspondants seront initialisés par le C. L'assembleur n'aura pas à se préoccuper de cela. Ensuite, la bibliothèque du C pourra être utilisée par le code assembleur. Les routines d'E/S de l'auteur en tirent parti. Elles utilisent les fonctions d'E/S du C (printf, etc.). L'exemple suivant montre un programme assembleur simple.

first.asm

Sélectionnez

asm
1 ; fichier : first.asm
2 ; Premier programme assembleur. Ce programme attend la saisie de deux
3 ; entiers et affiche leur somme.
4 ;
5 ; Pour creer l'executable en utilisant djgpp :
6 ; nasm -f coff first.asm
7 ; gcc -o first first.o driver.c asm_io.o
8
9 %include "asm_io.inc"
10 ;
11 ; Les donnees initialisees sont placees dans le segment .data
12 ;
13 segment .data
14 ;
15 ; Ces labels referencent les chaines utilisees pour l'affichage
16 ;
17 prompt1 db "Entrez un nombre : ", 0 ; N'oubliez pas le 0 final
18 prompt2 db "Entrez un autre nombre : ", 0
19 outmsg1 db "Vous avez entre ", 0
20 outmsg2 db " et ", 0
21 outmsg3 db ", leur somme vaut ", 0
22
23 ;
24 ; Les donnees non initialisees sont placees dans le segment .bss
25 ;
26 segment .bss
27 ;
28 ; Ces labels referencent les doubles-mots utilises pour stocker les entrees
29 ;
30 input1 resd 1
31 input2 resd 1
32
33 ;
34 ; Le code est place dans le segment .text
35 ;
36 segment .text
37 global _asm_main
38 _asm_main :
39 enter 0,0 ; initialisation
40 pusha
41
42 mov eax, prompt1 ; affiche un message
43 call print_string
44
45 call read_int ; lit un entier
46 mov [input1], eax ; le stocke dans input1
47
48 mov eax, prompt2 ; affiche un message
49 call print_string
50
51 call read_int ; lit un entier
52 mov [input2], eax ; le stocke dans input2
53
54 mov eax, [input1] ; eax = dword en input1
55 add eax, [input2] ; eax += dword en input2
56 mov ebx, eax ; ebx = eax
57
58 dump_regs 1 ; affiche les valeurs des registres
59 dump_mem 2, outmsg1, 1 ; affiche le contenu de la memoire
60 ;
61 ; Ce qui suit affiche le message resultat en plusieurs etapes
62 ;
63 mov eax, outmsg1
64 call print_string ; affiche le premier message
65 mov eax, [input1]
66 call print_int ; affiche input1
67 mov eax, outmsg2
68 call print_string ; affiche le second message
69 mov eax, [input2]
70 call print_int ; affiche input2
71 mov eax, outmsg3
72 call print_string ; affiche le troisieme message
73 mov eax, ebx
74 call print_int ; affiche la somme (ebx)
75 call print_nl ; affiche une nouvelle ligne
76
77 popa
78 mov eax, 0 ; retourne dans le programme C
79 leave
80 ret

La ligne 13 du programme définit une section qui spécifie la mémoire à stocker dans le segment de données (dont le nom est .data). Seules les données initialisées doivent être définies dans ce segment.

Dans les lignes 17 à 21, plusieurs chaînes sont déclarées. Elles seront affichées avec la bibliothèque C et doivent donc se terminer par un caractère null (code ASCII 0). Souvenez-vous qu'il y a une grande différence entre 0 et '0'.

Les données non initialisées doivent être déclarées dans le segment bss (appelé .bss à la ligne 26). Ce segment tient son nom d'un vieil opérateur assembleur UNIX qui signifiait block started by symbol. Il y a également un segment de pile. Nous en parlerons plus tard.

Le segment de code est appelé .text historiquement. C'est là que les instructions sont placées. Notez que le label de code pour la routine main (ligne 38) a un préfixe de soulignement. Cela fait partie de la convention d'appel C. Cette convention spécifie les règles que le C utilise lorsqu'il compile le code. Il est très important de connaître cette convention lorsque l'on interface du C et de l'assembleur. Plus loin, la convention sera présentée dans son intégralité ; cependant, pour l'instant, il suffit de savoir que tous les symboles C (i.e., les fonctions et les variables globales) ont un préfixe de soulignement qui leur est ajouté par le compilateur C (cette règle s'applique spécifiquement pour DOS/Windows, le compilateur C Linux n'ajoute rien du tout aux noms des symboles).

La directive global ligne 37 indique à l'assembleur de rendre le label _asm_main global. Contrairement au C, les labels ont une portée interne par défaut. Cela signifie que seul le code du même module peut utiliser le label.

La directive global donne au(x) label(s) spécifié(s) une portée externe. Ce type de label peut être accédé par n'importe quel module du programme.

Le module asm_io déclare les labels print_int, et.al. comme étant globaux. C'est pourquoi l'on peut les utiliser dans le module first.asm.

II-D-2. Dépendance vis-à-vis du compilateur

Le code assembleur ci-dessus est spécifique au compilateur C/C++ GNU (9) gratuit DJGPP(10).

Ce compilateur peut être téléchargé gratuitement depuis Internet. Il nécessite un PC à base de 386 ou plus et tourne sous DOS, Windows 95/98 ou NT. Ce compilateur utilise des fichiers objet au format COFF (Common Object File Format). Pour assembler le fichier au format COFF, utilisez l'option -f coff avec nasm (comme l'indiquent les commentaires du code). L'extension du fichier objet sera o.

Le compilateur C Linux est également un compilateur GNU. Pour convertir le code ci-dessus afin qu'il tourne sous Linux, retirez simplement les préfixes de soulignement aux lignes 37 et 38. Linux utilise le format ELF (Executable and Linkable Format) pour les fichiers objet. Utilisez l'option -f elf pour Linux. Il produit également un fichier objet avec l'extension o.

Les fichiers spécifiques aucompilateur, disponibles sur le site de l'auteur, ont déjà été modifiés pour fonctionner avec le compilateur approprié.

Borland C/C++ est un autre compilateur populaire. Il utilise le format Microsoft OMF pour les fichiers objets. Utilisez l'option -f obj pour les compilateurs Borland. L'extension du fichier objet sera obj. Le format OMF utilise des directives segment différentes de celles des autres formats objet.

Le segment data (ligne 13) doit être changé en :

Sélectionnez

segment _DATA public align=4 class=DATA use32

Le segment bss (ligne 26) doit être changé en :

Sélectionnez

segment _BSS public align=4 class=BSS use32

Le segment text (ligne 36) doit être changé en :

Sélectionnez

segment _TEXT public align=1 class=CODE use32

De plus, une nouvelle ligne doit être ajoutée avant la ligne 36 :

Sélectionnez

group DGROUP _BSS _DATA

Le compilateur Microsoft C/C++ peut utiliser soit le format OMF, soit le format Win32 pour les fichiers objet (si on lui passe un format OMF, il convertit les informations au format Win32 en interne). Le format Win32 permet de définir les segments comme pour DJGPP et Linux. Utilisez l'option -f win32 pour produire un fichier objet dans ce format. L'extension du fichier objet sera obj.

II-D-3. Assembler le code

La première étape consiste à assembler le code. Depuis la ligne de commande, saisissez :

Sélectionnez

nasm -f format-objet first.asm

où format-objet est soit co , soit elf , soit obj soit win32 selon le compilateur C utilisé (souvenez-vous que le fichier source doit être modifié pour Linux et pour Borland).

II-D-4. Compiler le code C

Compilez le fichier driver.c en utilisant un compilateur C. Pour DJGPP, utilisez :

Sélectionnez

gcc -c driver.c

L'option -c signifie de compiler uniquement, sans essayer de lier. Cette option fonctionne à la fois sur les compilateurs Linux, Borland et Microsoft.

II-D-5. Lier les fichiers objet

L'édition de liens est le procédé qui consiste à combiner le code machine et les données des fichiers objet et des bibliothèques afin de créer un fichier exécutable. Comme nous allons le voir, ce processus est compliqué.

Le code C nécessite la bibliothèque standard du C et un code de démarrage spécial afin de s'exécuter. Il est beaucoup plus simple de laisser le compilateur C appeler l'éditeur de liens avec les paramètres corrects que d'essayer d'appeler l'éditeur de liens directement. Par exemple, pour lier le code du premier programme en utilisant DJGPP, utilisez :

Sélectionnez

gcc -o first driver.o first.o asm_io.o

Cela crée un exécutable appelé first.exe (ou juste first sous Linux).

Avec Borland, on utiliserait :

Sélectionnez

bcc32 first.obj driver.obj asm_io.obj

Borland utilise le nom du premier fichier afin de déterminer le nom de l'exécutable. Donc, dans le cas ci-dessus, le programme s'appellera first.exe.

Il est possible de combiner les étapes de compilation et d'édition de liens.

Par exemple :

Sélectionnez

gcc -o first driver.c first.o asm_io.o

Maintenant gcc compilera driver.c puis liera.

II-D-6. Comprendre un listing assembleur

L'option -l fichier-listing peut être utilisée pour indiquer à nasm de créer un fichier listing avec le nom donné. Ce fichier montre comment le code a été assemblé. Voici comment les lignes 17 et 18 (dans le segment data) apparaissent dans le fichier listing (les numéros de ligne sont dans le

fichier listing ; cependant, notez que les numéros de ligne dans le fichier source peuvent ne pas être les mêmes).

Sélectionnez

48 00000000 456E7465722061206E- prompt1 db "Entrez un nombre : ", 0
49 00000009 756D6265723A2000
50 00000011 456E74657220616E6F- prompt2 db "Entrez un autre nombre : ", 0
51 0000001A 74686572206E756D62-
52 00000023 65723A2000

Les nombres diffèrent sur la version française, car ce ne sont pas les mêmes caractères.

La première colonne de chaque ligne est le numéro de la ligne et la seconde est le déplacement (en hexa) de la donnée dans le segment. La troisième colonne montre les données hexa brutes qui seront stockées. Dans ce cas, les données hexa correspondent aux codes ASCII. Enfin, le texte du fichier source est affiché sur la droite. Les déplacements listés dans la seconde colonne ne sont très probablement pas les déplacements réels auxquels les données seront placées dans le programme terminé. Chaque module peut définir ses propres labels dans le segment de données (et les autres segments également). Lors de l'étape d'édition de liens (voir Section II.D.5), toutes ces définitions de labels de segment de données sont combinées pour ne former qu'un segment de données. Les déplacements finals sont alors calculés par l'éditeur de liens.

Voici une petite portion (lignes 54 à 56 du fichier source) du segment text dans le fichier listing :

Sélectionnez

94 0000002C A1[00000000] mov eax, [input1]
95 00000031 0305[04000000] add eax, [input2]
96 00000037 89C3 mov ebx, eax

La troisième colonne montre le code machine généré par l'assembleur. Souvent le code complet pour une instruction ne peut pas encore être calculé.

Par exemple, ligne 94, le déplacement (ou l'adresse) de input1 n'est pas connu avant que le code ne soit lié. L'assembleur peut calculer l'opcode pour l'instruction mov (qui, d'après le listing, est A1), mais il écrit le déplacement entre crochets, car la valeur exacte ne peut pas encore être calculée. Dans ce cas, un déplacement temporaire de 0 est utilisé, car input1 est au début du segment bss déclaré dans ce fichier. Souvenez-vous que cela ne signifie pas qu'il sera au début du segment bss final du programme. Lorsque le code est lié, l'éditeur de liens insérera le déplacement correct à la place.

D'autres instructions, comme ligne 96, ne font référence à aucun label. Dans ce cas, l'assembleur peut calculer le code machine complet.

Représentations Big et Little Endian

Endian est prononcé comme indian.

Si l'on regarde attentivement la ligne 95, quelque chose semble très bizarre à propos du déplacement entre crochets dans le code machine. Le label input2 est au déplacement 4 (comme défini dans ce fichier) ; cependant, le déplacement qui apparaît en mémoire n'est pas 00000004, mais 04000000.

Pourquoi ? Des processeurs différents stockent les entiers multioctets dans des ordres différents en mémoire. Il y a deux méthodes populaires pour stocker : big endian et little endian. Le big endian est la méthode qui semble la plus naturelle. L'octet le plus fort (i.e. le plus significatif) est stocké en premier, puis le second plus fort, etc. Par exemple, le dword 00000004 serait stocké sous la forme des quatre octets suivants : 00 00 00 04.

Les mainframes IBM, la plupart des processeurs RISC et les processeurs Motorola utilisent tous cette méthode big endian. Cependant, les processeurs de type Intel utilisent la méthode little endian ! Ici, l'octet le moins significatif est stocké en premier. Donc, 00000004 est stocké en mémoire sous la forme 04 00 00 00. Ce format est codé en dur dans le processeur et ne peut pas être changé. Normalement, le programmeur n'a pas besoin de s'inquiéter du format utilisé. Cependant, il y a des circonstances où c'est important.

1. Lorsque des données binaires sont transférées entre différents ordinateurs (soit via des fichiers, soit via un réseau).

2. Lorsque des données binaires sont écrites en mémoire comme un entier multioctet puis relues comme des octets individuels ou vice versa.

Le caractère big ou little endian ne s'applique pas à l'ordre des éléments d'un tableau. Le premier élément d'un tableau est toujours à l'adresse la plus petite. C'est également valable pour les chaînes (qui sont juste des tableaux de caractères). Cependant, le caractère big ou little endian s'applique toujours aux éléments individuels des tableaux.

II-E. Fichier squelette

La Figure 1.7 montre un fichier squelette qui peut être utilisé comme point de départ pour l'écriture de programmes assembleur.

Image non disponible

III. Bases du langage assembleur

III-A. Travailler avec les entiers

III-A-1. Représentation des entiers

Les entiers se décomposent en deux catégories : signés et non signés. Les entiers non signés (qui sont positifs) sont représentés d'une manière binaire très intuitive. Le nombre 200 en tant qu'entier non signé sur un octet serait représenté par 11001000 (ou C8 en hexa).

Les entiers signés (qui peuvent être positifs ou négatifs) sont représentés d'une façon plus compliquée. Par exemple, considérons −56. +56 serait représenté par l'octet 00111000. Sur papier, on peut représenter −56 comme −111000, mais comment cela serait-il représenté dans un octet en mémoire de l'ordinateur. Comment serait stocké le signe moins ?

Il y a trois techniques principales qui ont été utilisées pour représenter les entiers signés dans la mémoire de l'ordinateur. Toutes ces méthodes utilisent le bit le plus significatif de l'entier comme bit de signe. Ce bit vaut 0 si le nombre est positif et 1 s'il est négatif.

III-A-1-a. Grandeur signée

La première méthode est la plus simple, elle est appelée grandeur signée.

Elle représente l'entier en deux parties. La première partie est le bit de signe et la seconde est la grandeur entière. Donc 56 serait représenté par l'octet 00111000 (le bit de signe est souligné) et −56 serait 10111000. La valeur d'octet la plus grande serait 01111111 soit +127 et la plus petite valeur sur un octet serait 11111111 soit −127. Pour obtenir l'opposé d'une valeur, on inverse le bit de signe. Cette méthode est intuitive, mais a ses inconvénients. Tout d'abord, il y a deux valeurs possibles pour 0 : +0 (00000000) et −0 (10000000). Comme zéro n'est ni positif ni négatif, ces deux représentations devraient se comporter de la même façon. Cela complique la logique de l'arithmétique pour le processeur. De plus, l'arithmétique générale est également compliquée. Si l'on ajoute 10 à −56, cela doit être transformé en 10 moins 56. Là encore, cela complique la logique du processeur.

III-A-1-b. Complément à 1

La seconde méthode est appelée représentation en complément à un.

Le complément à un d'un nombre est trouvé en inversant chaque bit du nombre (une autre façon de l'obtenir est que la valeur du nouveau bit est 1 − anciennevaleurdubit). Par exemple, le complément à un de 00111000 (+56) est 11000111. Dans la notation en complément à un, calculer le complément à un est équivalent à la négation. Donc 11000111 est la représentation de −56. Notez que le bit de signe a été automatiquement changé par le complément à un et que, comme l'on s'y attendait, appliquer le complément à un deux fois redonne le nombre de départ. Comme pour la première méthode, il y a deux représentations pour 0 : 00000000 (+0) et 11111111 (−0).

L'arithmétique des nombres en complément à 1 est compliquée. Voici une astuce utile pour trouver le complément à 1 d'un nombre en hexadécimal sans repasser en binaire. L'astuce est d'ôter le chiffre hexa de F (ou 15 en décimal). Cette méthode suppose que le nombre de bits dans le nombre est multiple de 4. Voici un exemple : +56 est représenté par 38 en hexa. Pour trouver le complément à 1, ôtez chaque chiffre de F pour obtenir C7 en hexa. Cela concorde avec le résultat ci-dessus.

III-A-1-c. Complément à 2

Les deux premières méthodes décrites ont été utilisées sur les premiers ordinateurs. Les ordinateurs modernes utilisent une troisième méthode appelée représentation en complément à 2. On trouve le complément à 2 d'un nombre en effectuant les deux opérations suivantes :

1. Trouver le complément à un du nombre ;

2. Ajouter 1 au résultat de l'étape 1.

Voici un exemple en utilisant 00111000 (56). Tout d'abord, on calcule le complément à 1 : 11000111. Puis, on ajoute 1 :

11000111

+ 1

11001000

Dans la notation en complément à 2, calculer le complément à 2 est équivalent à trouver l'opposé d'un nombre. Donc, 11001000 est la représentation en complément à 2 de −56. Deux négations devraient reproduire le nombre original. Curieusement le complément à 2 ne remplit pas cette condition. Prenez le complément à 2 de 11001000 en ajoutant 1 au complément à 1.

00110111

+ 1

00111000

Lorsque l'on effectue l'addition dans l'opération de complémentation à 2, l'addition du bit le plus à gauche peut produire une retenue. Cette retenue n'est pas utilisée. Souvenez-vous que toutes les données sur un ordinateur sont d'une taille fixe (en termes de nombre de bits). Ajouter deux octets produit toujours un résultat sur un octet (comme ajouter deux mots donne un mot, etc.) Cette propriété est importante pour la notation en complément à 2. Par exemple, considérons zéro comme un nombre en complément à 2 sur un octet (00000000). Calculer son complément à 2 produit la somme :

11111111

+ 1

c 00000000

où c représente une retenue (plus tard, nous montrerons comment détecter cette retenue, mais elle n'est pas stockée dans le résultat). Donc, en notation en complément à 2, il n'y a qu'un zéro. Cela rend l'arithmétique en complément à 2 plus simple que les méthodes précédentes.

En utilisant la notation en complément à 2, un octet signé peut être utilisé pour représenter les nombres −128 à +127.

Image non disponible

Le Tableau 2.1 montre quelques valeurs choisies. Si 16 bits sont utilisés, les nombres signés −32 768 à +32 767 peuvent être représentés. +32 767 est représenté par 7FFF, −32 768

par 8000, -128 par FF80 et -1 par FFFF. Les nombres en complément à 2 sur 32 bits vont de −2 milliards à +2 milliards environ.

Le processeur n'a aucune idée de ce qu'un octet en particulier (ou un mot ou un double-mot) est supposé représenté. L'assembleur n'a pas le concept de types qu'un langage de plus haut niveau peut avoir. La façon dont les données sont interprétées dépend de l'instruction dans laquelle on les utilise. Que la valeur FF soit considérée comme représentant un −1 signé ou 1 +255 non signé dépend du programmeur. Le langage C définit des types entiers signés et non signés. Cela permet au compilateur C de déterminer les instructions correctes à utiliser avec les données.

III-A-2. Extension de signe

En assembleur, toutes les données ont une taille bien spécifiée. Il n'est pas rare de devoir changer la taille d'une donnée pour l'utiliser avec d'autres. Réduire la taille est le plus simple.

III-A-2-a. Réduire la taille des données

Pour réduire la taille d'une donnée, il suffit d'en retirer les bits les plus significatifs. Voici un exemple trivial :

Sélectionnez

mov ax, 0034h ; ax = 52 (stocké sur 16 bits)
mov cl, al ; cl = les 8 bits de poids faible de ax

Bien sûr, si le nombre ne peut pas être représenté correctement dans une plus petite taille, réduire la taille ne fonctionne pas. Par exemple, si AX valait 0134h (ou 308 en décimal) alors le code ci-dessus mettrait quand même CL à 34h. Cette méthode fonctionne à la fois avec les nombres signés et non signés. Considérons les nombres signés, si AX valait FFFFh (−1 sur un mot), alors CL vaudrait FFh (−1 sur un octet). Cependant, notez que si la valeur dans AX était signée, elle aurait été tronquée et le résultat aurait été faux !

La règle pour les nombres non signés est que tous les bits retirés soient à 0 afin que la conversion soit correcte. La règle pour les nombres signés est que les bits retirés doivent soit tous être des 1 soit tous des 0. De plus, le premier bit à ne pas être retiré doit valoir la même chose que ceux qui l'ont été. Ce bit sera le bit de signe pour la valeur plus petite. Il est important qu'il ait la même valeur que le bit de signe original !

III-A-2-b. Augmenter la taille des données

Augmenter la taille des données est plus compliqué que la réduire. Considérons l'octet hexa FF. S'il est étendu à un mot, quelle valeur aura-t-il ? Cela dépend de la façon dont FF est interprété. Si FF est un octet non signé (255 en décimal), alors le mot doit être 00FF ; cependant, s'il s'agit d'un octet signé (−1 en décimal), alors le mot doit être FFFF.

En général, pour étendre un nombre non signé, on met tous les bits supplémentaires du nouveau nombre à 0. Donc, FF devient 00FF. Cependant, pour étendre un nombre signé, on doit étendre le bit de signe. Cela signifie que les nouveaux bits deviennent des copies du bit de signe. Comme le bit de signe de FF est à 1, les nouveaux bits doivent également être des 1, pour donner FFFF. Si le nombre signé 5A (90 en décimal) était étendu, le résultat serait 005A.

Il y a plusieurs instructions fournies par le 80386 pour étendre les nombres.

Souvenez-vous que l'ordinateur ne sait pas si un nombre est signé ou non. C'est au programmeur d'utiliser l'instruction adéquate. Pour les nombres non signés, on peut placer des 0 dans les bits de poids fort de façon simple en utilisant l'instruction MOV. Par exemple pour étendre l'octet de AL en un mot non signé dans AX :

Sélectionnez

mov ah, 0 ; met les 8 bits de poids fort à 0

Cependant, il n'est pas possible d'utiliser une instruction MOV pour convertir le mot non signé de AX en un double-mot non signé dans EAX. Pourquoi pas ? Il n'y a aucune façon d'accéder aux 16 bits de poids fort de EAX dans un MOV. Le 80386 résout ce problème en fournissant une nouvelle instruction : MOVZX. Cette instruction a deux opérandes. La destination (premier opérande) doit être un registre de 16 ou 32 bits. La source (deuxième opérande) doit être un registre 8 ou 16 bits. L'autre restriction est que la destination doit être plus grande que la source (la plupart des instructions requièrent que la source soit de la même taille que la destination). Voici quelques exemples :

Sélectionnez

movzx eax, ax ; étend ax à eax

movzx eax, al ; étend al à eax

movzx ax, al ; étend al à ax

movzx ebx, ax ; étend ax à ebx

Pour les nombres signés, il n'y a pas de façon simple d'utiliser l'instruction MOV quel que soit le cas. Le 8086 fournit plusieurs instructions pour étendre les nombres signés. L'instruction CBW (Convert Byte to Word, Convertir un Octet en Mot) étend le signe du registre AL dans AX. Les opérandes sont implicites. L'instruction CWD (Convert Word to Double word, Convertir un Mot en Double-mot) étend le signe de AX dans DX:AX. La notation DX:AX signifie qu'il faut considérer les registres DX et AX comme un seul registre 32 bits avec les 16 bits de poids forts dans DX et les bits de poids faible dans AX (souvenez-vous que le 8086 n'avait pas de registre 32 bits du tout !). Le 80386 a apporté plusieurs nouvelles instructions. L'instruction CWDE (Convert Word to Double word Extended, convertir un mot en double mot étendue) étend le signe de AX dans EAX. L'instruction CDQ (Convert Double word to Quad word, Convertir un Double-mot en Quadruple-mot) étend le signe de EAX dans EDX:EAX (64 bits !) Enn, l'instruction MOVSX fonctionne comme MOVZX excepté qu'elle utilise les règles des nombres signés.

III-A-2-c. Application à la programmation en C

L'extension d'entiers signés et non signés a également lieu en C. Les variables en C peuvent être déclarées soit comme signées soit comme non signées (les int sont signés).

Image non disponible

Le C ANSI ne définit pas si le type char est signé ou non, c'est à chaque compilateur de le décider. C'est pourquoi on définit le type explicitement dans la Figure 2.1.

Considérons le code de la Figure 2.1. À la ligne 3, la variable a est étendue en utilisant les règles pour les valeurs non signées (avec MOVZX), mais à la ligne 4, les règles signées sont utilisées pour b (avec MOVSX).

Il existe un bogue de programmation courant en C qui est directement lié à notre sujet. Considérons le code de la Figure 2.2. Le prototype de fgetc()est :

Sélectionnez

int fgetc( FILE * );

On peut se demander pourquoi la fonction renvoie un int puisqu'elle lit des caractères. La raison est qu'elle renvoie normalement un char (étendu à une valeur int en utilisant l'extension de zéro). Cependant, il y a une valeur qu'elle peut retourner qui n'est pas un caractère, EOF. C'est une macro habituellement définie comme valant −1. Donc, fgetc() retourne soit un char étendu à une valeur int (qui ressemble à 000000xx en hexa) soit EOF (qui ressemble à FFFFFFFF en hexa).

Le problème avec le programme de la Figure 2.2 est que fgetc() renvoie un int, mais cette valeur est stockée dans un char. Le C tronquera alors les bits de poids fort pour faire tenir la valeur de l'int dans le char. Le seul problème est que les nombres (en hexa) 000000FF et FFFFFFFF seront tous deux tronqués pour donner l'octet FF. Donc, la boucle while ne peut pas distinguer la lecture de l'octet FF dans le fichier et la fin de ce fichier.

Ce que fait exactement le code dans ce cas change selon que le char est signé ou non. Pourquoi ? Parce que ligne 2, ch est comparé avec EOF. Comme EOF est un int (11), ch sera étendu à un int afin que les deux valeurs comparées soient de la même taille(12). Comme la Figure 2.1 le montrait, le fait qu'une variable soit signée ou non est très important.

Si le char n'est pas signé, FF est étendu et donne 000000FF. Cette valeur est comparée à EOF (FFFFFFFF) et est différente. Donc la boucle ne finit jamais !

Si le char est signé, FF est étendu et donne FFFFFFFF. Les deux valeurs sont alors égales et la boucle se termine. Cependant, comme l'octet FF peut avoir été lu depuis le fichier, la boucle peut se terminer prématurément.

La solution à ce problème est de définir la variable ch comme un int, pas un char. Dans ce cas, aucune troncature ou extension n'est effectuée à la ligne 2. Dans la boucle, il est sans danger de tronquer la valeur puisque ch doit alors être réellement un octet.

III-A-3. Arithmétique en complément à 2

Comme nous l'avons vu plus tôt, l'instruction add effectue une addition et l'instruction sub effectue une soustraction. Deux des bits du registre EFLAGS que ces instructions positionnent sont overow (dépassement de capacité) et carry ag (retenue). Le drapeau d'overow est à 1 si le résultat réel de l'opération est trop grand et ne tient pas dans la destination pour l'arithmétique signée. Le drapeau de retenue est à 1 s'il y a une retenue dans le bit le plus significatif d'une addition d'une soustraction. Donc, il peut être utilisé pour détecter un dépassement de capacité en arithmétique non signée. L'utilisation du drapeau de retenue pour l'arithmétique signée va être vue sous peu. Un des grands avantages du complément à 2 est que les règles pour l'addition et la soustraction sont exactement les mêmes que pour les entiers non signés. Donc, add et sub peuvent être utilisées pour les entiers signés ou non.

002C 44

+ FFFF + (−1)

002B 43

Il y a une retenue de générée, mais elle ne fait pas partie de la réponse.

Il y a deux instructions de multiplication et de division différentes. Tout d'abord, pour multiplier, utilisez les instructions MUL ou IMUL. L'instruction MUL est utilisée pour multiplier les nombres non signés et IMUL est utilisée pour multiplier les entiers signés. Pourquoi deux instructions différentes sont nécessaires ? Les règles pour la multiplication sont différentes pour les nombres non signés et les nombres signés en complément à 2. Pourquoi cela ?

Image non disponible

Considérons la multiplication de l'octet FF avec lui-même donnant un résultat sur un mot. En utilisant la multiplication non signée, il s'agit de 255 fois 255 soit 65 025 (ou FE01 en hexa). En utilisant la multiplication signée, il s'agit de −1 fois −1 soit 1 (ou 0001 en hexa).

Il y a plusieurs formes pour les instructions de multiplication. La plus ancienne ressemble à cela :

Sélectionnez

mul source

La source est soit un registre soit une référence mémoire. Cela ne peut pas être une valeur immédiate. Le type exact de la multiplication dépend de la taille de l'opérande source. Si l'opérande est un octet, elle est multipliée par l'octet situé dans le registre AL et le résultat est stocké dans les 16 bits de AX. Si la source fait 16 bits, elle est multipliée par le mot situé dans AX et le résultat 32 bits est stocké dans DX:AX. Si la source fait 32 bits, elle est multipliée par EAX et le résultat 64 bits est stocké dans EDX:EAX.

L'instruction IMUL a les mêmes formats que MUL, mais ajoute également d'autres formats d'instruction. Il y a des formats à deux et à trois opérandes :

Sélectionnez

imul dest, source1

imul dest, source1, source2

Le Tableau 2.2 montre les combinaisons possibles.

Les deux opérateurs de division sont DIV et IDIV. Ils effectuent respectivement une division entière non signée et une division entière signée. Le format général est :

Sélectionnez

div source

Si la source est sur 8 bits, alors AX est divisé par l'opérande. Le quotient est stocké dans AL et le reste dans AH. Si la source est sur 16 bits, alors DX:AX est divisé par l'opérande. Le quotient est stocké dans AX et le reste dans DX. Si la source est sur 32 bits, alors EDX:EAX est divisé par l'opérande, le quotient est stocké dans EAX et le reste dans EDX. L'instruction IDIV fonctionne de la même façon. Il n'y a pas d'instructions IDIV spéciales comme pour IMUL. Si le quotient est trop grand pour tenir dans son registre ou que le diviseur vaut 0, le programme et interrompu et se termine. Une erreur courante est d'oublier d'initialiser DX ou EDX avant la division.

L'instruction NEG inverse son opérande en calculant son complément à 2. L'opérande peut être n'importe quel registre ou emplacement mémoire sur 8 bits, 16 bits, or 32 bits.

III-A-4. Programme exemple

math.asm

Sélectionnez

1 %include "asm_io.inc"
2 segment .data ; Chaines affichees
3 prompt db "Entrez un nombre : ", 0
4 square_msg db "Le carre de l'entree vaut ", 0
5 cube_msg db "Le cube de l'entree vaut ", 0
6 cube25_msg db "Le cube de l'entree fois 25 vaut ", 0
7 quot_msg db "Le quotient de cube/100 vaut ", 0
8 rem_msg db "Le reste de cube/100 vaut ", 0
9 neg_msg db "La négation du reste vaut ", 0
10
11 segment .bss
12 input resd 1
13
14 segment .text
15 global _asm_main
16 _asm_main :
17 enter 0,0 ; routine d'initialisation
18 pusha
19
20 mov eax, prompt
21 call print_string
22
23 call read_int
24 mov [input], eax
25
26 imul eax ; edx:eax = eax * eax
27 mov ebx, eax ; sauvegarde le résultat dans ebx
28 mov eax, square_msg
29 call print_string
30 mov eax, ebx
31 call print_int
32 call print_nl
33
34 mov ebx, eax
35 imul ebx, [input] ; ebx *= [entree]
36 mov eax, cube_msg
37 call print_string
38 mov eax, ebx
39 call print_int
40 call print_nl
41
42 imul ecx, ebx, 25 ; ecx = ebx*25
43 mov eax, cube25_msg
44 call print_string
45 mov eax, ecx
46 call print_int
47 call print_nl
48
49 mov eax, ebx
50 cdq ; initialise edx avec extension de signe
51 mov ecx, 100 ; on ne peut pas diviser par une valeur immédiate
52 idiv ecx ; edx:eax / ecx
53 mov ecx, eax ; sauvegarde le résultat dans ecx
54 mov eax, quot_msg
55 call print_string
56 mov eax, ecx
57 call print_int
58 call print_nl
59 mov eax, rem_msg
60 call print_string
61 mov eax, edx
62 call print_int
63 call print_nl
64
65 neg edx ; inverse le reste
66 mov eax, neg_msg
67 call print_string
68 mov eax, edx
69 call print_int
70 call print_nl
71
72 popa
73 mov eax, 0 ; retour au C
74 leave
75 ret

III-A-5. Arithmétique en précision étendue

Le langage assembleur fournit également des instructions qui permettent d'effectuer des additions et des soustractions sur des nombres plus grands que des doubles mots. Ces instructions utilisent le drapeau de retenue. Comme nous l'avons dit plus haut, les instructions ADD et SUB modifient le drapeau de retenue si une retenue est générée. Cette information stockée dans le drapeau de retenue peut être utilisée pour additionner ou soustraire de grands nombres en morcelant l'opération en doubles-mots (ou plus petit).

Les instructions ADC et SBB utilisent les informations données par le drapeau de retenue. L'instruction ADC effectue l'opération suivante :

opérande1 = opérande1 + drapeau de retenue + opérande2

L'instruction SBB effectue :

opérande1 = opérande1 - drapeau de retenue - opérande2

Comment sont-elles utilisées ? Considérons la somme d'entiers 64 bits dans EDX:EAX et EBX:ECX. Le code suivant stockerait la somme dans EDX:EAX :

Sélectionnez

1 add eax, ecx ; additionne les 32 bits de poids faible

2 adc edx, ebx ; additionne les 32 bits de poids fort et la retenue

La soustraction est très similaire. Le code suivant soustrait EBX:ECX de EDX:EAX :

Sélectionnez

1 sub eax, ecx ; soustrait les 32 bits de poids faible

2 sbb edx, ebx ; soustrait les 32 bits de poids fort et la retenue

Pour les nombres vraiment grands, une boucle peut être utilisée (voir Section III.B). Pour une boucle de somme, il serait pratique d'utiliser l'instruction ADC pour toutes les itérations (au lieu de toutes sauf la première).

Cela peut être fait en utilisant l'instruction CLC (CLear Carry) juste avant que la boucle ne commence pour initialiser le drapeau de retenue à 0. Si le drapeau de retenue vaut 0, il n'y a pas de différence entre les instructions ADD et ADC. La même idée peut être utilisée pour la soustraction.

III-B. Structures de contrôle

Les langages de haut niveau fournissent des structures de contrôle de haut niveau (p.e., les instructions if et while) qui contrôlent le flot d'exécution. Le langage assembleur ne fournit pas de structures de contrôle aussi complexes. Il utilise à la place l'infâme goto et l'utiliser de manière inappropriée peut conduire à du code spaghetti ! Cependant, il est possible d'écrire des programmes structurés en assembleur. La procédure de base est de concevoir la logique du programme en utilisant les structures de contrôle de haut niveau habituelles et de traduire cette conception en langage assembleur (comme le ferait un compilateur).

III-B-1. Comparaison

Les structures de contrôle décident ce qu'il faut faire en comparant des données. En assembleur, le résultat d'une comparaison est stocké dans le registre FLAGS pour être utilisé plus tard. Le 80x86 fournit l'instruction CMP pour effectuer des comparaisons. Le registre FLAGS est positionné selon la différence entre les deux opérandes de l'instruction CMP. Les opérandes sont soustraits et le registre FLAGS est positionné selon le résultat, mais ce résultat n'est stocké nulle part. Si vous avez besoin du résultat, utilisez l'instruction SUB au lieu de CMP.

Pour les entiers non signés, il y a deux drapeaux (bits dans le registre FLAGS) qui sont importants : les drapeaux zéro (ZF) et retenue (CF). Le drapeau zero est allumé (1) si le résultat de la différence serait 0. Considérons une comparaison comme :

Sélectionnez

cmp vleft, vright

La différence vleft - vright est calculée et les drapeaux sont positionnés en fonction. Si la différence calculée par CMP vaut 0, vleft = vright, alors ZF est allumé(i.e. 1) et CF est éteint (i.e. 0). Si vleft > vright, ZF est éteint et CF également. Si vleft < vright, alors ZF est éteint et CF est allumé (retenue).

Pour les entiers signés, il y a trois drapeaux importants : le drapeau zéro (ZF), le drapeau d'overow (OF) et le drapeau de signe (SF). Le drapeau d'overow est allumé si le résultat d'une opération dépasse la capacité (de trop ou de trop peu). Le drapeau de signe est allumé si le résultat d'une opération est négatif. Si vleft = vright, ZF est allumé (comme pour les entiers non signés). Si vleft > vright, ZF est éteint et SF = OF. Si vleft < vright, ZF est éteint et SF 6= OF.

N'oubliez pas que d'autres instructions peuvent aussi changer le registre FLAGS, pas seulement CMP.

Pourquoi SF = OF si vleft > vright ? S'il n'y
a pas d'overow, alors la différence aura la valeur correcte et doit être positive ou nulle. Donc, SF = OF = 0. Par contre, s'il y a un overow, la différence n'aura pas la valeur correcte (et sera en fait négative).
Donc, SF = OF = 1.

III-B-2. Instructions de branchement

Les instructions de branchement peuvent transférer l'exécution à n'importe quel point du programme. En d'autres termes, elles agissent comme un goto. Il y a deux types de branchements : conditionnel et inconditionnel. Un branchement inconditionnel est exactement comme un goto, il effectue toujours le branchement. Un branchement conditionnel peut effectuer le branchement ou non selon les drapeaux du registre FLAGS. Si un branchement conditionnel n'effectue pas le branchement, le contrôle passe à l'instruction suivante.

L'instruction JMP (abréviation de jump) effectue les branchements inconditionnels.

Son seul argument est habituellement l'étiquette de code de l'instruction à laquelle se brancher. L'assembleur ou l'éditeur de liens remplacera l'étiquette par l'adresse correcte de l'instruction. C'est une autre des opérations pénibles que l'assembleur fait pour rendre la vie du programmeur plus simple. Il est important de réaliser que l'instruction immédiatement après l'instruction JMP ne sera jamais exécutée à moins qu'une autre instruction ne se branche dessus !

Il y a plusieurs variantes de l'instruction de saut :

SHORT ce saut est très limité en portée. Il ne peut bouger que de plus ou moins 128 octets en mémoire. L'avantage de ce type de saut est qu'il utilise moins de mémoire que les autres. Il utilise un seul octet signé pour stocker le déplacement du saut. Le déplacement est le nombre d'octets à sauter vers l'avant ou vers l'arrière (le déplacement est ajouté à EIP). Pour spécifier un saut court, utilisez le mot-clé SHORT immédiatement avant l'étiquette dans l'instruction JMP ;

NEAR ce saut est le saut par défaut, que ce soit pour les branchements inconditionnels ou conditionnels, il peut être utilisé pour sauter vers n'importe quel emplacement dans un segment. En fait, le 80386 supporte deux types de sauts proches. L'un utilise deux octets pour le déplacement. Cela permet de se déplacer en avant ou en arrière d'environ 32 000 octets. L'autre type utilise quatre octets pour le déplacement, ce qui, bien sûr, permet de se déplacer n'importe où dans le segment.

Le type de saut à deux octets peut être spécifié en plaçant le mot-clé WORD avant l'étiquette dans l'instruction JMP.

FAR ce saut permet de passer le contrôle à un autre segment de code. C'est une chose très rare en mode protégé 386.

Les étiquettes de code valides suivent les mêmes règles que les étiquettes de données. Les étiquettes de code sont définies en les plaçant dans le segment de code au début de l'instruction qu'ils étiquettent. Deux points sont placés à la fin de l'étiquette lors de sa définition. Ces deux points ne font pas partie du nom.

Image non disponible

Il y a beaucoup d'instructions de branchement conditionnel différentes. Elles prennent toutes une étiquette de code comme seul opérande. Les plus simples se contentent de regarder un drapeau dans le registre FLAGS pour déterminer si elles doivent sauter ou non. Voyez le Tableau 2.3 pour une liste de ces instructions (PF est le drapeau de parité qui indique si le nombre de bits dans les 8 bits de poids faible du résultat est pair ou impair).

Le pseudo-code suivant :

Sélectionnez

if ( EAX == 0 )

EBX = 1;

else

EBX = 2;

peut être écrit de cette façon en assembleur :

Sélectionnez

1 cmp eax, 0 ; positionne les drapeaux (ZF allumé si eax - 0 = 0)

2 jz thenblock ; si ZF est allumé, branchement vers thenblock

3 mov ebx, 2 ; partie ELSE du IF

4 jmp next ; saute par dessus la partie THEN du IF

5 thenblock :

6 mov ebx, 1 ; partie THEN du IF

7 next :

Les autres comparaisons ne sont pas si simples si l'on se contente des branchements conditionnels du Tableau 2.3. Pour illustrer, considérons le pseudo-code suivant :

Sélectionnez

if ( EAX >= 5 )

EBX = 1;

else

EBX = 2;

Si EAX est plus grand ou égal à 5, ZF peut être allumé ou non et SF sera égal à OF. Voici le code assembleur qui teste ces conditions (en supposant que EAX est signé) :

Sélectionnez

1 cmp eax, 5
2 js signon ; goto signon si SF = 1
3 jo elseblock ; goto elseblock si OF = 1 et SF = 0
4 jmp thenblock ; goto thenblock si SF = 0 et OF = 0
5 signon:
6 jo thenblock ; goto thenblock si SF = 1 et OF = 1
7 elseblock :
8 mov ebx, 2
9 jmp next
10 thenblock:
11 mov ebx, 1
12 next:

Le code ci-dessus est très confus. Heureusement, le 80x86 fournit des instructions de branchement additionnelles pour rendre ce type de test beaucoup plus simple. Il y a des versions signées et non signées de chacune.

Image non disponible

Le Tableau 2.4 montre ces instructions. Les branchements égal et non égal (JE et JNE) sont les mêmes pour les entiers signés et non signés (en fait, JE et JNE sont réellement identiques à JZ et JNZ, respectivement). Chacune des autres instructions de branchement a deux synonymes. Par exemple, regardez JL (jump less than, saut si inférieur à) et JNGE (jump not greater than or equal to, saut si non plus grand ou égal). Ce sont les mêmes instructions, car :

x < y => not(x y)

Les branchements non signés utilisent A pour above (au-dessus) et B pour below (en dessous) à la place de L et G.

En utilisant ces nouvelles instructions de branchement, le pseudo-code ci-dessus peut être traduit en assembleur plus facilement.

Sélectionnez

1 cmp eax, 5
2 jge thenblock
3 mov ebx, 2
4 jmp next
5 thenblock:
6 mov ebx, 1
7 next:

III-B-3. Les instructions de boucle

Le 80x86 fournit plusieurs instructions destinées à implémenter des boucles de style for. Chacune de ces instructions prend une étiquette de code comme seul opérande.

LOOP décrémente ECX, si ECX 6= 0, saute vers l'étiquette.

LOOPE, LOOPZ décrémente ECX (le registre FLAGS n'est pas modifié), si ECX 6= 0 et ZF = 1, saute.

LOOPNE, LOOPNZ décrémente ECX (FLAGS inchangé), si ECX 6= 0 et ZF = 0, saute.

Les deux dernières instructions de boucle sont utiles pour les boucles de recherche séquentielle. Le pseudo-code suivant :

Sélectionnez

sum = 0;

for ( i=10; i >0; i&#8722;&#8722; )

sum += i;

peut être traduit en assembleur de la façon suivante ;

Sélectionnez

1 mov eax, 0 ; eax est sum
2 mov ecx, 10 ; ecx est i
3 loop_start:
4 add eax, ecx
5 loop loop_start

III-C. Traduire les structures de contrôle standard

Cette section décrit comment les structures de contrôle standard des langages de haut niveau peuvent être implémentées en langage assembleur.

III-C-1. Instructions if

Le pseudo-code suivant :

Sélectionnez

if ( condition )
then_block;
else
else_block;

peut être implémenté de cette façon :

Sélectionnez

1 ; code pour positionner FLAGS
2 jxx else_block ; choisir xx afin de sauter si la condition est fausse
3 ; code pour le bloc then
4 jmp endif
5 else_block :
6 ; code pour le bloc else
7 endif :

S'il n'y a pas de else, le branchement else_block peut être remplacé par un branchement à endif.

Sélectionnez

1 ; code pour positionner FLAGS
2 jxx endif ; choisir xx afin de sauter si la condition est fausse
3 ; code pour le bloc then
4 endif :

III-C-2. Boucles while

La boucle while est une boucle avec un test au début :

Sélectionnez

while( condition ) {

corps de la boucle;

}

Cela peut être traduit en :

Sélectionnez

1 while :
2 ; code pour positionner FLAGS selon la condition
3 jxx endwhile ; choisir xx pour sauter si la condition est fausse
4 ; body of loop
5 jmp while
6 endwhile :

III-C-3. Boucles do while

La boucle do while est une boucle avec un test à la fin :

Sélectionnez

do {

corps de la boucle;

} while( condition );

Cela peut être traduit en :

Sélectionnez

1 do :

2 ; corps de la boucle

3 ; code pour positionner FLAGS selon la condition

4 jxx do ; choisir xx pour se brancher si la condition est vraie

III-D. Exemple : trouver des nombres premiers

Cette section détaille un programme qui trouve des nombres premiers.

Rappelez-vous que les nombres premiers ne sont divisibles que par 1 et par eux-mêmes. Il n'y a pas de formule pour les rechercher. La méthode de base qu'utilise ce programme est de rechercher les diviseurs de tous les nombres impairs (13) sous une limite donnée. Si aucun facteur ne peut être trouvé pour un nombre impair, il est premier. Voici l'algorithme de base écrit en C.

Sélectionnez

1 unsigned guess; / candidat courant pour être premier /

2 unsigned factor ; / diviseur possible de guess /

3 unsigned limit ; / rechercher les nombres premiers jusqu'à cette valeur /

4

5  printf ("Rechercher les nombres premiers jusqu'a : ");

6 scanf("%u", &limit);

7 printf ("2\n"); / traite les deux premiers nombres premiers comme /

8 printf ("3\n"); / des cas spéciaux /

9 guess = 5; / candidat initial /

10 while ( guess <= limit ) {

11 / recherche un diviseur du candidat /

12 factor = 3;

13 while ( factor factor < guess &&

14 guess % factor != 0 )

15 factor += 2;

16 if ( guess % factor != 0 )

17 printf ("%d\n", guess);

18 guess += 2; / ne regarder que les nombres impairs /

19 }

Voici la version assembleur :

prime.asm

Sélectionnez

1 %include "asm_io.inc"
2 segment .data
3 Message db "Rechercher les nombres premiers jusqu'a : ", 0
4
5  segment .bss
6 Limit resd 1 ; rechercher les nombres premiers jusqu'à cette limite
7 Guess resd 1 ; candidat courant pour être premier
8
9  segment .text
10 global _asm_main
11 _asm_main :
12 enter 0,0 ; routine d'initialisation
13 pusha
14
15 mov eax, Message
16 call print_string
17 call read_int ; scanf("%u", &limit);
18 mov [Limit], eax
19
20 mov eax, 2 ; printf("2\n");
21 call print_int
22 call print_nl
23 mov eax, 3 ; printf("3\n");
24 call print_int
25 call print_nl
26
27 mov dword [Guess], 5 ; Guess = 5;
28 while_limit : ; while ( Guess <= Limit )
29 mov eax,[Guess]
30 cmp eax, [Limit]
31 jnbe end_while_limit ; on utilise jnbe, car les nombres ne sont pas signes
32
33 mov ebx, 3 ; ebx est factor = 3;
34 while_factor:
35 mov eax,ebx
36 mul eax ; edx:eax = eax*eax
37 jo end_while_factor ; si la réponse ne tient pas dans eax seul
38 cmp eax, [Guess]
39 jnb end_while_factor ; si !(factor*factor < guess)
40 mov eax,[Guess]
41 mov edx,0
42 div ebx ; edx = edx:eax % ebx
43 cmp edx, 0
44 je end_while_factor ; si !(guess % factor != 0)
45
46 add ebx,2 ; factor += 2;
47 jmp while_factor
48 end_while_factor:
49 je end_if ; si !(guess % factor != 0)
50 mov eax,[Guess] ; printf("%u\n")
51 call print_int
52 call print_nl
53 end_if :
54 add dword [Guess], 2 ; guess += 2
55 jmp while_limit
56 end_while_limit:
57
58 popa
59 mov eax, 0 ; retour au C
60 leave
61 ret

IV. Opérations sur les bits

IV-A. Opérations de décalage

Le langage assembleur permet au programmeur de manipuler individuellement les bits des données. Une opération courante sur les bits est appelée décalage. Une opération de décalage déplace la position des bits d'une donnée. Les décalages peuvent être soit vers la gauche (i.e. vers les bits les plus significatifs) soit vers la droite (les bits les moins significatifs).

IV-A-1. Décalages logiques

Le décalage logique est le type le plus simple de décalage. Il décale d'une manière très simple. La Figure 3.1 montre l'exemple du décalage d'un nombre sur un octet.

Image non disponible

Notez que les nouveaux bits sont toujours à 0. Les instructions SHL et SHR sont respectivement utilisées pour décaler à gauche et à droite. Ces instructions permettent de décaler de n'importe quel nombre de positions.

Le nombre de positions à décaler peut soit être une constante, soit être stocké dans le registre CL. Le dernier bit décalé de la donnée est stocké dans le drapeau de retenue. Voici quelques exemples :

Sélectionnez

1 mov ax, 0C123H

2 shl ax, 1 ; décale d'1 bit à gauche, ax = 8246H, CF = 1

3 shr ax, 1 ; décale d'1 bit à droite, ax = 4123H, CF = 0

4 shr ax, 1 ; décale d'1 bit à droite, ax = 2091H, CF = 1

5 mov ax, 0C123H

6 shl ax, 2 ; décale de 2 bits à gauche, ax = 048CH, CF = 1

7 mov cl, 3

8 shr ax, cl ; décale de 3 bits à droite, ax = 0091H, CF = 1

IV-A-2. Utilisation des décalages

La multiplication et la division rapides sont les utilisations les plus courantes des opérations de décalage. Rappelez-vous que dans le système décimal, la multiplication et la division par une puissance de dix sont simples, on décale simplement les chiffres. C'est également vrai pour les puissances de deux en binaire. Par exemple, pour multiplier par deux le nombre binaire 10112 (ou 11 en décimal), décalez d'un cran vers la gauche pour obtenir 101102 (ou 22). Le quotient d'une division par une puissance de 2 est le résultat d'un décalage à droite. Pour diviser par 2, utilisez un décalage d'une position à droite ; pour diviser par 4 (22), décalez à droite de deux positions ; pour diviser par 8 (23), décalez de 3 positions vers la droite, etc. Les instructions de décalage sont très basiques et sont beaucoup plus rapides que les instructions MUL et DIV correspondantes !

En fait, les décalages logiques peuvent être utilisés pour multiplier ou diviser des valeurs non signées. Ils ne fonctionnent généralement pas pour les valeurs signées. Considérons la valeur sur deux octets FFFF (−1 signé). Si on lui applique un décalage logique d'une position vers la droite, le résultat est 7FFF ce qui fait +32 767 ! Un autre type de décalage doit être utilisé pour les valeurs signées.

IV-A-3. Décalages arithmétiques

Ces décalages sont conçus pour permettre à des nombres signés d'être rapidement multipliés et divisés par des puissances de 2. Ils assurent que le bit de signe est traité correctement.

SAL Shift Arithmetic Left (décalage arithmétique à gauche) - Cette instruction est juste un synonyme pour SHL. Elle est assemblée pour donner exactement le même code machine que SHL. Tant que le bit de signe n'est pas changé par le décalage, le résultat sera correct.

SAR Shift Arithmetic Right (décalage arithmétique à droite) - C'est une nouvelle instruction qui ne décale pas le bit de signe (i.e. le msb) de son opérande. Les autres bits sont décalés normalement sauf que les nouveaux bits qui entrent par la gauche sont des copies du bit de signe (c'est-à-dire que si le bit de signe est à 1, les nouveaux bits sont également à 1). Donc, si un octet est décalé avec cette instruction, seuls les 7 bits de poids faible sont décalés. Comme pour les autres décalages, le dernier bit sorti est stocké dans le drapeau de retenue.

Sélectionnez

1 mov ax, 0C123H

2 sal ax, 1 ; ax = 8246H, CF = 1

3 sal ax, 1 ; ax = 048CH, CF = 1

4 sar ax, 2 ; ax = 0123H, CF = 0

IV-A-4. Décalages circulaires

Les instructions de décalage circulaire fonctionnent comme les décalages logiques excepté que les bits perdus à un bout sont réintégrés à l'autre. Donc, la donnée est traitée comme s'il s'agissait d'une structure circulaire. Les deux instructions de rotation les plus simples sont ROL et ROR qui effectuent des rotations à gauche et à droite, respectivement. Comme pour les autres décalages, ceux-ci laissent une copie du dernier bit sorti dans le drapeau de retenue.

Sélectionnez

1 mov ax, 0C123H

2 rol ax, 1 ; ax = 8247H, CF = 1

3 rol ax, 1 ; ax = 048FH, CF = 1

4 rol ax, 1 ; ax = 091EH, CF = 0

5 ror ax, 2 ; ax = 8247H, CF = 1

6 ror ax, 1 ; ax = C123H, CF = 1

Il y a deux instructions de rotation supplémentaires qui décalent les bits de la donnée et le drapeau de retenue appelés RCL et RCR. Par exemple, si on applique au registre AX une rotation avec ces instructions, elle est appliquée aux 17 bits constitués de AX et du drapeau de retenue.

Sélectionnez

1 mov ax, 0C123H

2 clc ; eteint le drapeau de retenue (CF = 0)

3 rcl ax, 1 ; ax = 8246H, CF = 1

4 rcl ax, 1 ; ax = 048DH, CF = 1

5 rcl ax, 1 ; ax = 091BH, CF = 0

6 rcr ax, 2 ; ax = 8246H, CF = 1

7 rcr ax, 1 ; ax = C123H, CF = 0

IV-A-5. Application simple

Voici un extrait de code qui compte le nombre de bits qui sont allumés (i.e. à 1) dans le registre EAX.

Sélectionnez

1 mov bl, 0 ; bl contiendra le nombre de bits ALLUMES
2 mov ecx, 32 ; ecx est le compteur de boucle
3 count_loop:
4 shl eax, 1 ; decale un bit dans le drapeau de retenue
5 jnc skip_inc ; si CF == 0, goto skip_inc
6 inc bl
7 skip_inc :
8 loop count_loop

Le code ci-dessus détruit la valeur originale de EAX (EAX vaut zéro à la fin de la boucle). Si l'on voulait conserver la valeur de EAX, la ligne 4 pourrait être remplacée par rol eax, 1.

IV-B. Opérations booléennes niveau bit

Image non disponible

Il y a quatre opérateurs booléens courants : AND, OR, XOR et NOT. Une table de vérité montre le résultat de chaque opération pour chaque valeur possible de ses opérandes.

IV-B-1. L'opération ET

Le résultat d'un ET sur deux bits vaut 1 uniquement si les deux bits sont à 1, sinon, le résultat vaut 0, comme le montre la table de vérité du Tableau 3.1.

Les processeurs supportent ces opérations comme des instructions agissant de façon indépendante sur tous les bits de la donnée en parallèle. Par exemple, si on applique un ET au contenu de AL et BL, l'opération ET de base est appliquée à chacune des 8 paires de bits correspondantes dans les deux registres, comme le montre la Figure 3.2. Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H

2 and ax, 82F6H ; ax = 8022H

IV-B-2. L'opération OU

Image non disponible

Le OU de 2 bits vaut 0 uniquement si les deux bits valent 0, sinon le résultat vaut 1 comme le montre la table de vérité du Tableau 3.2. Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H
2 or ax, 0E831H ; ax = E933H

IV-B-3. L'opération XOR

Le OU exclusif de deux bits vaut 0 uniquement si les deux bits sont égaux, sinon, le résultat vaut 1 comme le montre la table de vérité du Tableau 3.3.

Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H

2 xor ax, 0E831H ; ax = 2912H

IV-B-4. L'opération NOT

Image non disponible

L'opération NOT est une opération unaire (i.e. elle agit sur un seul opérande, pas deux comme les opérations binaires de type ET). Le NOT d'un bit est la valeur opposée du bit comme le montre la table de vérité du Tableau 3.4. Voici un exemple de code :

Sélectionnez

1 mov ax, 0C123H

2 not ax ; ax = 3EDCH

Notez que le NOT donne le complément à un. Contrairement aux autres opérations niveau bit, l'instruction NOT ne change aucun des bits du registre FLAGS.

IV-B-5. L'instruction TEST

L'instruction TEST effectue une opération AND, mais ne stocke pas le résultat. Elle positionne le registre FLAGS selon ce que ce dernier aurait été après un AND (comme l'instruction CMP qui effectue une soustraction, mais ne fait que positionner FLAGS). Par exemple, si le résultat était zéro, ZF serait allumé.

IV-B-6. Utilisation des opérations sur les bits

Les opérations sur les bits sont très utiles pour manipuler individuellement les bits d'une donnée sans modifier les autres bits. Le Tableau 3.5 montre trois utilisations courantes de ces opérations.

Image non disponible

Voici un exemple de code, implémentant ces idées.

Sélectionnez

1 mov ax, 0C123H

2 or ax, 8 ; allumer le bit 3, ax = C12BH

3 and ax, 0FFDFH ; éteindre le bit 5, ax = C10BH

4 xor ax, 8000H ; inverser le bit 31, ax = 410BH

5 or ax, 0F00H ; allumer un quadruplet, ax = 4F0BH

6 and ax, 0FFF0H ; éteindre un quadruplet, ax = 4F00H

7 xor ax, 0F00FH ; inverser des quadruplets, ax = BF0FH

8 xor ax, 0FFFFH ; complément à 1, ax = 40F0H

L'opération ET peut aussi être utilisée pour trouver le reste d'une division par une puissance de deux. Pour trouver le reste d'une division par 2i, effectuez un ET sur le nombre avec un masque valant 2i − 1. Ce masque contiendra des 1 du bit 0 au bit i−1. Ce sont tout simplement ces bits qui correspondent au reste. Le résultat du ET conservera ces bits et mettra les autres à zéro. Voici un extrait de code qui trouve le quotient et le reste de la division de 100 par 16.

Sélectionnez

1 mov eax, 100 ; 100 = 64H

2 mov ebx, 0000000FH ; masque = 16 - 1 = 15 ou F

3 and ebx, eax ; ebx = reste = 4

En utilisant le registre CL il est possible de modifier n'importe quel(s) bit(s) d'une donnée. Voici un exemple qui allume un bit de EAX. Le numéro du bit à allumer est stocké dans BH.

Sélectionnez

1 mov cl, bh ; tout d'abord, construire le nombre pour le OU

2 mov ebx, 1

3 shl ebx, cl ; décalage à gauche cl fois

4 or eax, ebx ; allume le bit

Éteindre un bit est un tout petit peut plus dur.

Sélectionnez

1 mov cl, bh ; tout d'abord, construire le nombre pour le ET

2 mov ebx, 1

3 shl ebx, cl ; décalage à gauche cl fois

4 not ebx ; inverse les bits

5 and eax, ebx ; éteint le bit

Le code pour inverser un bit est laissé en exercice au lecteur.

Il n'est pas rare de voir l'instruction déroutante suivante dans un programme 80x86 :

Sélectionnez

xor eax, eax ; eax = 0

Un nombre auquel on applique un XOR avec lui-même donne toujours zéro.

Cette instruction est utilisée, car son code machine est plus petit que l'instruction MOV correspondante.

IV-C. Éviter les branchements conditionnels

Les processeurs modernes utilisent des techniques très sophistiquées pour exécuter le code le plus rapidement possible. Une technique répandue est appelée exécution spéculative. Cette technique utilise les possibilités de traitement en parallèle du processeur pour exécuter plusieurs instructions à la fois. Les branchements conditionnels posent un problème à ce type de fonctionnement.

Le processeur, en général, ne sait pas si le branchement sera effectué ou pas. Selon qu'il est effectué ou non, un ensemble d'instructions différent sera exécuté. Les processeurs essaient de prévoir si le branchement sera effectué. Si la prévision est mauvaise, le processeur a perdu son temps en exécutant le mauvais code.

Une façon d'éviter ce problème est d'éviter d'utiliser les branchements conditionnels lorsque c'est possible. Le code d'exemple de III.A.5 fournit un exemple simple de la façon de le faire. Dans l'exemple précédent, les bits allumés du registre EAX sont décomptés. Il utilise un branchement pour éviter l'instruction INC. La Figure 3.3 montre comment le branchement peut être retiré en utilisant l'instruction ADC pour ajouter directement le drapeau de retenue.

Image non disponible

Les instructions SETxx fournissent un moyen de retirer les branchements dans certains cas. Ces instructions positionnent la valeur d'un registre ou d'un emplacement mémoire d'un octet à zéro ou à un selon l'état du registre FLAGS. Les caractères après SET sont les mêmes que pour les branchements conditionnels. Si la condition correspondante au SETxx est vraie, le résultat stocké est un, s'il est faux, zéro est stocké. Par exemple :

Sélectionnez

setz al ; AL = 1 si ZF est allume, sinon 0

En utilisant ces instructions, il est possible de développer des techniques ingénieuses qui calculent des valeurs sans branchement.

Par exemple, considérons le problème de la recherche de la plus grande de deux valeurs. L'approche standard pour résoudre ce problème serait d'utiliser un CMP et un branchement conditionnel pour déterminer la valeur la plus grande. Le programme exemple ci-dessous montre comment le maximum peut être trouvé sans utiliser aucun branchement.

Sélectionnez

1 ; file : max.asm
2 %include "asm_io.inc"
3 segment .data
4
5  message1 db "Entrez un nombre : ",0
6 message2 db "Entrez un autre nombre : ", 0
7 message3 db "Le plus grand nombre est : ", 0
8
9  segment .bss
10
11 input1 resd 1 ; premier nombre entre
12
13 segment .text
14 global _asm_main
15 _asm_main :
16 enter 0,0 ; routine d'initialisation
17 pusha
18
19 mov eax, message1 ; affichage du premier message
20 call print_string
21 call read_int ; saisie du premier nombre
22 mov [input1], eax
23
24 mov eax, message2 ; affichage du second message
25 call print_string
26 call read_int ; saisie du second nombre (dans eax)
27
28 xor ebx, ebx ; ebx = 0
29 cmp eax, [input1] ; compare le premier et le second nombre
30 setg bl ; ebx = (input2 > input1) ? 1 : 0
31 neg ebx ; ebx = (input2 > input1) ? 0xFFFFFFFF : 0
32 mov ecx, ebx ; ecx = (input2 > input1) ? 0xFFFFFFFF : 0
33 and ecx, eax ; ecx = (input2 > input1) ? input2 : 0
34 not ebx ; ebx = (input2 > input1) ? 0 : 0xFFFFFFFF
35 and ebx, [input1] ; ebx = (input2 > input1) ? 0 : input1
36 or ecx, ebx ; ecx = (input2 > input1) ? input2 : input1
37
38 mov eax, message3 ; affichage du résultat
39 call print_string
40 mov eax, ecx
41 call print_int
42 call print_nl
43
44 popa
45 mov eax, 0 ; retour au C
46 leave
47 ret

L'astuce est de créer un masque de bits qui peut être utilisé pour sélectionner la valeur correcte pour le maximum. L'instruction SETG à la ligne 30 positionne BL à 1 si la seconde saisie est la plus grande ou à 0 sinon. Ce n'est pas le masque de bits désiré.

Pour créer le masque nécessaire, la ligne 31 utilise l'instruction NEG sur le registre EBX (notez que EBX a été positionné à 0 précédemment). Si EBX vaut 0, cela ne fait rien ; cependant, si EBX vaut 1, le résultat est la représentation en complément à 2 de -1 soit 0xFFFFFFFF. C'est exactement le masque de bits désiré.

Le code restant utilise ce masque de bits pour sélectionner la saisie correspondant au plus

grand nombre.

Une autre astuce est d'utiliser l'instruction DEC. Dans le code ci-dessus, si NEG est remplacé par DEC, le résultat sera également 0 ou 0xFFFFFFFF. Cependant, les valeurs sont inversées par rapport à l'utilisation de l'instruction NEG.

IV-D. Manipuler les bits en C

IV-D-1. Les opérateurs niveau bit du C

Contrairement à certains langages de haut niveau, le C fournit des opérateurs pour les opérations niveau bit. L'opération ET est représentée par l'opérateur binaire & (14). L'opération OU est représentée par l'opérateur binaire |. L'opération XOR est représentée par l'opérateur binaire ^ . Et l'opération NOT est représentée par l'opérateur unaire ~ .

Les opérations de décalage sont effectuées au moyen des opérateurs binaires et du C. L'opérateur effectue les décalages à gauche et l'opérateur effectue les décalages à droite. Ces opérateurs prennent deux opérandes. L'opérande de gauche est la valeur à décaler et l'opérande de droite est le nombre de bits à décaler. Si la valeur à décaler est d'un type non signé, un décalage logique est effectué. Si la valeur est d'un type signé (comme int), alors un décalage arithmétique est utilisé. Voici un exemple en C utilisant ces opérateurs :

Sélectionnez

1 short int s ; / on suppose que les short int font 16 bits /

2 short unsigned u;

3 s = &#8722;1; / s = 0xFFFF (complement a 2) /

4 u = 100; / u = 0x0064 /
5 u = u | 0x0100; / u = 0x0164 /
6 s = s & 0xFFF0; / s = 0xFFF0 /
7 s = s ^ u; / s = 0xFE94 /
8 u = u << 3; / u = 0x0B20 (décalage logique) /
9 s = s >> 2; / s = 0xFFA5 (décalage arithmetique) /

IV-D-2. Utiliser les opérateurs niveau bit en C

Les opérateurs niveau bit sont utilisés en C pour les mêmes raisons qu'ils le sont en assembleur. Ils permettent de manipuler les bits d'une donnée individuellement et peuvent être utilisés pour des multiplications et des divisions rapides. En fait, un compilateur C malin utilisera automatiquement un décalage pour une multiplication du type x *= 2.

Beaucoup d'API (15) de systèmes d'exploitation (comme POSIX(16) et Win32) contiennent des fonctions qui utilisent des opérandes donc les données sont codées sous la forme de bits. Par exemple, les systèmes POSIX conservent les permissions sur les fichiers pour trois différents types d'utilisateurs : utilisateur (un nom plus approprié serait propriétaire), groupe et autres. Chaque type d'utilisateur peut recevoir la permission de lire, écrire et/ou exécuter un fichier. Pour changer les permissions d'un fichier, le programmeur C doit manipuler des bits individuels. POSIX définit plusieurs macros pour l'aider (voir Tableau 3.6).

Image non disponible

La fonction chmod peut être utilisée pour définir les permissions sur un fichier. Cette fonction prend deux paramètres, une chaine avec le nom du fichier à modifier et un entier (17) avec les bits appropriés d'allumés pour les permissions désirées. Par exemple, le code ci-dessous définit les permissions pour permettre au propriétaire du fichier de lire et d'écrire dedans, au groupe de lire le fichier et d'interdire l'accès aux autres.

Sélectionnez

chmod("foo", S_IRUSR | S_IWUSR | S_IRGRP );

La fonction POSIX stat peut être utilisée pour récupérer les bits de permission en cours pour un fichier. En l'utilisant avec la fonction chmod,

il est possible de modifier certaines des permissions sans changer les autres.

Voici un exemple qui retire l'accès en écriture aux autres et ajoute les droits de lecture pour le propriétaire. Les autres permissions ne sont pas altérées.

Sélectionnez

1 struct stat le_stats ; / structure utilisee par stat () /

2 stat ("foo", & le_stats ); / lit les infos du fichier

3 le_stats .st_mode contient les bits de permission /

4 chmod("foo", ( le_stats .st_mode & ~S_IWOTH) | S_IRUSR);

IV-E. Représentations big et little endian

Le Chapitre II a introduit les concepts de représentations big et little endian des données multioctets. Cependant, l'auteur s'est rendu compte que pour beaucoup de gens, ce sujet est confus. Cette section couvre le sujet plus en détail.

Le lecteur se souvient sûrement que le caractère big ou little endian fait référence à l'ordre dans lequel les octets (pas les bits) d'un élément de données multioctets sont stockés en mémoire. La représentation big endian est la méthode la plus intuitive. Elle stocke l'octet le plus significatif en premier, puis le second octet le plus significatif, etc. En d'autres termes, les gros (big) bits sont stockés en premier. La méthode little endian stocke les octets dans l'ordre inverse (moins significatif en premier). La famille des processeurs x86 utilise la représentation little endian.

Par exemple, considérons le double mot représentant 1234567816. En représentation big endian, les octets seraient stockés 12 34 56 78. En représentation little endian, les octets seraient stockés 78 56 34 12.

Le lecteur est probablement en train de se demander pourquoi n'importe quel concepteur de puce sain d'esprit utiliserait la représentation little endian ?

Les ingénieurs de chez Intel étaient-ils sadiques pour infliger à une multitude de programmeurs cette représentation qui prête à confusion ? Il peut sembler que le processeur ait à faire du travail supplémentaire pour stocker les octets en mémoire dans l'ordre inverse (et pour les réinverser lorsqu'il lit à partir de la mémoire). En fait, le processeur ne fait aucun travail supplémentaire pour lire et écrire en mémoire en utilisant le format little endian. Il faut comprendre que le processeur est constitué de beaucoup de circuits électroniques qui ne travaillent que sur des valeurs de un bit. Les bits (et les octets) peuvent être dans n'importe quel ordre dans le processeur.

Considérons le registre de deux octets AX. Il peut être décomposé en deux registres d'un octet : AH et AL. Il y a des circuits dans le processeur qui conservent les valeurs de AH and AL. Ces circuits n'ont pas d'ordre dans le processeur. C'est-à-dire que les circuits pour AH ne sont pas avant ou après les circuits pour AL. Une instruction MOV qui copie la valeur de AX en mémoire copie la valeur de AL puis de AH. Ce n'est pas plus dur pour le processeur que de stocker AH en premier.

Le même argument s'applique aux bits individuels d'un octet. Ils ne sont pas réellement dans un ordre déterminé dans les circuits du processeur (ou en mémoire en ce qui nous concerne). Cependant, comme les bits individuels ne peuvent pas être adressés dans le processeur ou en mémoire, il n'y a pas de façon de savoir (et aucune raison de s'en soucier) l'ordre dans lequel ils sont conservés à l'intérieur du processeur.

Le code C de la Figure 3.4 montre comment le caractère big ou little endian d'un processeur peut être déterminé.

Image non disponible

Le pointeur p traite la variable word comme un tableau de caractères de deux éléments. Donc, p[0] correspond au premier octet de word en mémoire dont la valeur dépend du caractère big ou little endian du processeur.

IV-E-1. Quand se soucier du caractère big ou little endian

Pour la programmation courante, le caractère big ou little endian du processeur n'est pas important. Le moment le plus courant où cela devient important est lorsque des données binaires sont transférées entre différents systèmes informatiques. Cela se fait habituellement en utilisant un type quelconque de média physique (comme un disque) ou via un réseau. Comme les données ASCII sont sur un seul octet, le caractère big ou little endian n'est pas un problème.

Avec l'avènement des jeux de caractères multioctets comme UNICODE, le caractère big ou little endian devient important même pour les données texte. UNICODE supporte les deux types de représentation et a un mécanisme pour indiquer celle qui est utilisée pour représenter les données.

Tous les en-têtes internes de TCP/IP stockent les entiers au format big

endian (appelé ordre des octets réseau). Les bibliothèques TCP/IP offrent des fonctions C pour résoudre les problèmes de représentation big ou little endian d'une façon portable. Par exemple, la fonction htonl () convertit un double-mot (ou un entier long) depuis le format hôte vers le format réseau.

La fonction function ntohl () effectue la transformation inverse (18). Pour un système big endian les deux fonctions retournent leur paramètre inchangé. Cela permet d'écrire des programmes réseau qui compileront et s'exécuteront correctement sur n'importe quel système sans tenir compte de la représentation utilisée. Pour plus d'information sur les représentations big et little endian et la programmation réseau, voyez l'excellent livre de W. Richard Steven : UNIX Network Programming.

La Figure 3.5 montre une fonction C qui passe d'une représentation à l'autre pour un double mot.

Image non disponible

Le processeur 486 a introduit une nouvelle instruction machine appelée BSWAP qui inverse les octets de n'importe quel registre 32 bits. Par exemple,

Sélectionnez

bswap edx ; échange les octets de edx

L'instruction ne peut pas être utilisée sur des registres de 16 bits. Cependant, l'instruction XCHG peut être utilisée pour échanger les octets des registres 16 bits pouvant être décomposés en registres de 8 bits. Par exemple :

Sélectionnez

xchg ah,al ; échange les octets de ax

IV-F. Compter les bits

Plus haut, nous avons donné une technique intuitive pour compter le nombre de bits allumés dans un double-mot. Cette section décrit d'autres méthodes moins directes de le faire pour illustrer les opérations sur les bits dont nous avons parlé dans ce chapitre.

IV-F-1. Méthode une

La première méthode est très simple, mais pas évidente. La Figure 3.6 en montre le code.

Image non disponible

Comment fonctionne cette méthode ? À chaque itération de la boucle, un bit est éteint dans data. Quand tous les bits sont éteints ((i.e. lorsque data vaut zéro), la boucle s'arrête. Le nombre d'itérations requises pour mettre data à zéro est égal au nombre de bits dans la valeur originale de data.

La ligne 6 est l'endroit où un bit de data est éteint. Comment cela marche ? Considérons la forme générale de la représentation binaire de data et le 1 le plus à droite dans cette représentation. Par dénifition, chaque bit après ce 1 est à zéro. Maintenant, que sera la représentation de data - 1 ? Les bits à gauche du 1 le plus à droite seront les mêmes que pour data, mais à partir du 1 le plus à droite, les bits seront les compléments des bits originaux de data. Par exemple :

data = xxxxx10000

data - 1 = xxxxx01111

où les x sont les mêmes pour les deux nombres. Lorsque l'on applique un ET sur data avec data - 1, le résultat mettra le 1 le plus à droite de data à zéro et laissera les autres bits inchangés.

IV-F-2. Méthode deux

Un tableau de recherche peut également être utilisé pour contrer les bits de n'importe quel double-mot. L'approche intuitive serait de précalculer le nombre de bits de chaque double-mot et de le stocker dans un tableau.

Cependant, il y a deux problèmes relatifs à cette approche. Il y a à peu près 4 milliards de valeurs de doubles-mots ! Cela signifie que le tableau serait très gros et que l'initialiser prendrait beaucoup de temps (en fait, à moins que l'on ait l'intention d'utiliser le tableau plus de 4 milliards de fois, cela prendrait plus de temps de l'initialiser que cela n'en prendrait de calculer le nombre de bits avec la méthode un !).

Une méthode plus réaliste calculerait le nombre de bits pour toutes les valeurs possibles d'octet et les stockerait dans un tableau. Le double-mot peut alors être décomposé en quatre valeurs d'un octet. Le nombre de bits pour chacune de ces quatre valeurs d'un octet est recherché dans le tableau et additionné aux autres pour trouver le nombre de bits du double-mot original.

La Figure 3.7 montre le code implémentant cette approche.

Image non disponible

La fonction initialize_count_bits doit être appelée avant le premier appel à la fonction count_bits. Cette fonction initialise le tableau global byte_bit_count. La fonction count_bits ne considère pas la variable data comme un double-mot, mais comme un tableau de quatre octets. Donc, dword[0] est un des octets de data (soit le moins significatif, soit le plus significatif selon que le matériel est little ou big endian, respectivement).

Bien sûr, on peut utiliser une instruction comme :

(data >> 24) & 0x000000FF

pour trouver la valeur de l'octet le plus significatif et des opérations similaires pour les autres octets, cependant, ces opérations seraient plus lentes qu'une référence à un tableau.

Un dernier point, une boucle for pourrait facilement être utilisée pour calculer la somme des lignes 22 et 23. Mais, cela inclurait le supplément de l'initialisation de l'indice, sa comparaison après chaque itération et son incrémentation. Calculer la somme comme une somme explicite de quatre valeurs est plus rapide. En fait, un compilateur intelligent convertirait la version avec une boucle for en une somme explicite. Ce procédé de réduire ou éliminer les itérations d'une boucle est une technique d'optimisation de compilateur appelée loop unrolling (déroulage de boucle).

IV-F-3. Méthode trois

Il y a encore une méthode intelligente de compter les bits allumés d'une donnée. Cette méthode ajoute littéralement les 1 et les 0 de la donnée.

La somme est égale au nombre de 1 dans la donnée. Par exemple, considérons le comptage des 1 d'un octet stocké dans une variable nommée data. La première étape est d'effectuer l'opération suivante :

Sélectionnez

data = (data & 0x55) + ((data >> 1) & 0x55);

Pourquoi faire cela ? La constante hexa 0x55 vaut 01010101 en binaire. Dans le premier opérande de l'addition, on effectue un ET sur data avec elle, les bits sur des positions impaires sont supprimés. Le second opérande ((data>> 1) & 0x55) commence par déplacer tous les bits à des positions paires vers une position impaire et utilise le même masque pour supprimer ces bits.

Maintenant, le premier opérande contient les bits impairs et le second, les bits pairs de data. Lorsque ces deux opérandes sont additionnés, les bits pairs et les bits impairs de data sont additionnés. Par exemple, si data vaut 101100112, alors :

Image non disponible

L'addition à droite montre les bits additionnés ensemble. Les bits de l'octet sont divisés en deux champs de 2 bits pour montrer qu'il y a en fait quatre additions indépendantes. Comme la plus grande valeur que ces sommes peuvent prendre est deux, il n'est pas possible qu'une somme déborde de son champ et corrompe l'une des autres sommes.

Bien sûr, le nombre total de bits n'a pas encore été calculé. Cependant, la même technique que celle qui a été utilisée ci-dessus peut être utilisée pour calculer le total en une série d'étapes similaires. L'étape suivante serait :

data = (data & 0x33) + ((data >> 2) & 0x33);

En continuant l'exemple du dessus (souvenez-vous que data vaut maintenant 011000102).

Image non disponible

Il y a maintenant deux champs de 4 bits additionnés individuellement. La prochaine étape est d'additionner ces deux sommes de bits ensemble pour former le résultat final :

data = (data & 0x0F) + ((data >> 4) & 0x0F);

En utilisant l'exemple ci-dessus (avec data égale à 001100102) :

Image non disponible

Maintenant, data vaut 5 ce qui est le résultat correct. La Figure 3.8 montre une implémentation de cette méthode qui compte les bits dans un double-mot.

Image non disponible

Elle utilise une boucle for pour calculer la somme. Il serait plus rapide de dérouler la boucle, cependant, la boucle rend plus claire la façon dont la méthode se généralise à différentes tailles de données.

V. Sous-programmes

Ce chapitre explique comment utiliser des sous-programmes pour créer des programmes modulaires et s'interfacer avec des langages de haut niveau (comme le C). Les fonctions et les procédures sont des exemples de sous-programmes dans les langages de haut niveau.

Le code qui appelle le sous-programme et le sous-programme lui-même doivent se mettre d'accord sur la façon de se passer les données. Ces règles sur la façon de passer les données sont appelées conventions d'appel. Une grande partie de ce chapitre traitera des conventions d'appel standard du C qui peuvent être utilisées pour interfacer des sous-programmes assembleur avec des programmes C. Celles-ci (et d'autres conventions) passent souvent les adresses des données (i.e. des pointeurs) pour permettre au sous-programme d'accéder aux données en mémoire.

V-A. Adressage indirect

L'adressage indirect permet aux registres de se comporter comme des pointeurs. Pour indiquer qu'un registre est utilisé indirectement comme un pointeur, il est entouré par des crochets ([ ]). Par exemple :

Sélectionnez

1 mov ax, [Data] ; adressage mémoire direct normal d'un mot

2 mov ebx, Data ; ebx = & Data

3 mov ax, [ebx] ; ax = *ebx

Comme AX contient un mot, la ligne 3 lit un mot commençant à l'adresse stockée dans EBX. Si AX était remplacé par AL, un seul octet serait lu.

Il est important de réaliser que les registres n'ont pas de types comme les variables en C. Ce sur quoi EBX est censé pointer est totalement déterminé par les instructions utilisées. Si EBX est utilisé de manière incorrecte, il n'y aura souvent pas d'erreur signalée de la part de l'assembleur, cependant, le programme ne fonctionnera pas correctement. C'est une des nombreuses raisons pour lesquelles la programmation assembleur est plus sujette à erreur que la programmation de haut niveau.

Tous les registres 32 bits généraux (EAX, EBX, ECX, EDX) et d'index (ESI, EDI) peuvent être utilisés pour l'adressage indirect. En général, les registres 16 et 8 bits ne le peuvent pas.

V-B. Exemple de sous-programme simple

Un sous-programme est une unité de code indépendante qui peut être utilisée depuis différentes parties du programme. En d'autres termes, un sous-programme est comme une fonction en C. Un saut peut être utilisé pour appeler le sous-programme, mais le retour présente un problème. Si le sous-programme est destiné à être utilisé par différentes parties du programme, il doit revenir à la section de code qui l'a appelé. Donc, le retour du sous-programme ne peut pas être codé en dur par un saut vers une étiquette. Le code ci-dessous montre comment cela peut être réalisé en utilisant une forme indirecte de l'instruction JMP. Cette forme de l'instruction utilise la valeur d'un registre pour déterminer où sauter (donc, le registre agit plus comme un pointeur de fonction du C). Voici le premier programme du Chapitre II réécrit pour utiliser un sous-programme.

sub1.asm

Sélectionnez

1 ; fichier : sub1.asm
2 ; Programme d'exemple de sous-programme
3 %include "asm_io.inc"
4
5  segment .data
6 prompt1 db "Entrez un nombre : ", 0 ; ne pas oublier le zéro terminal
7 prompt2 db "Entrez un autre nombre : ", 0
8 outmsg1 db "Vous avez entré ", 0
9 outmsg2 db " et ", 0
10 outmsg3 db ", la somme des deux vaut ", 0
11
12 segment .bss
13 input1 resd 1
14 input2 resd 1
15
16 segment .text
17 global _asm_main
18 _asm_main :
19 enter 0,0 ; routine d'initialisation
20 pusha
21
22 mov eax, prompt1 ; affiche l'invite
23 call print_string
24
25 mov ebx, input1 ; stocke l'adresse de input1 dans ebx
26 mov ecx, ret1 ; stocke l'adresse de retour dans ecx
27 jmp short get_int ; lit un entier
28 ret1:
29 mov eax, prompt2 ; affiche l'invite
30 call print_string
31
32 mov ebx, input2
33 mov ecx, $ + 7 ; ecx = cette addresse + 7
34 jmp short get_int
35
36 mov eax, [input1] ; eax = dword dans input1
37 add eax, [input2] ; eax += dword dans input2
38 mov ebx, eax ; ebx = eax
39
40 mov eax, outmsg1
41 call print_string ; affiche le premier message
42 mov eax, [input1]
43 call print_int ; affiche input1
44 mov eax, outmsg2
45 call print_string ; affiche le second message
46 mov eax, [input2]
47 call print_int ; affiche input2
48 mov eax, outmsg3
49 call print_string ; affiche le troisième message
50 mov eax, ebx
51 call print_int ; affiche la somme (ebx)
52 call print_nl ; retour à la ligne
53
54 popa
55 mov eax, 0 ; retour au C
56 leave
57 ret
58 ; subprogram get_int
59 ; Paramètres :
60 ; ebx - addresse du dword dans lequel stocker l'entier
61 ; ecx - addresse de l'instruction vers laquelle retourner
62 ; Notes :
63 ; la valeur de eax est perdue
64 get_int:
65 call read_int
66 mov [ebx], eax ; stocke la saisie en mémoire
67 jmp ecx sub1.;asrmetour à l'appelant

Le sous-programme get_int utilise une convention d'appel simple, basée sur les registres. Il s'attend à ce que le registre EBX contienne l'adresse du DWORD dans lequel stocker le nombre saisi et à ce que le registre ECX contiennent l'adresse de l'instruction vers laquelle retourner.

Dans les lignes 25 à 28, l'étiquette ret1 est utilisée pour calculer cette adresse de retour.

Dans les lignes 32 à 34, l'opérateur $ est utilisé pour calculer l'adresse de retour. L'opérateur $ retourne l'adresse de la ligne sur laquelle il apparaît. L'expression $ + 7 calcule l'adresse de l'instruction MOV de la ligne 36.

Ces deux calculs d'adresses de code de retour sont compliqués. La première méthode requiert la définition d'une étiquette pour tout appel de sous-programme.

La seconde méthode ne requiert pas d'étiquette, mais nécessite de réfléchir attentivement. Si un saut proche avait été utilisé à la place d'un saut court, le nombre à ajouter à $ n'aurait pas été 7 !

Heureusement, il y a une façon plus simple d'appeler des sous-programmes. Cette méthode utilise la pile.

V-C. La pile

Beaucoup de processeurs ont un support intégré pour une pile. Une pile est une liste Last-In First-Out (LIFO, dernier entré, premier sorti). La pile est une zone de la mémoire qui est organisée de cette façon. L'instruction PUSH ajoute des données à la pile et l'instruction POP retire une donnée. La donnée retirée est toujours la dernière donnée ajoutée (c'est pourquoi on appelle ce genre de liste dernier entré, premier sorti).

Le registre de segment SS spécifie le segment qui contient la pile (habituellement c'est le même que celui qui contient les données). Le registre ESP contient l'adresse de la donnée qui sera retirée de la pile. On dit que cette donnée est au sommet de la pile. Les données ne peuvent être ajoutées que par unités de doubles-mots. C'est-à-dire qu'il est impossible de placer un octet seul sur la pile.

L'instruction PUSH insère un double-mot (19) sur la pile en ôtant 4 de ESP puis en stockant le double-mot en [ESP]. L'instruction POP lit le double-mot en [ESP] puis ajoute 4 à ESP. Le code ci-dessous montre comment fonctionnent ces instructions en supposant que ESP vaut initialement 1000H.

Sélectionnez

1 push dword 1 ; 1 est stocké en 0FFCh, ESP = 0FFCh

2 push dword 2 ; 2 est stocké en 0FF8h, ESP = 0FF8h

3 push dword 3 ; 3 est stocké en 0FF4h, ESP = 0FF4h

4 pop eax ; EAX = 3, ESP = 0FF8h

5 pop ebx ; EBX = 2, ESP = 0FFCh

6 pop ecx ; ECX = 1, ESP = 1000h

La pile peut être utilisée comme un endroit approprié pour stocker des données temporairement. Elle est également utilisée pour effectuer des appels de sous-programmes, passer des paramètres et des variables locales.

Le 80x86 fournit également une instruction, PUSHA, qui empile les valeurs des registres EAX, EBX, ECX, EDX, ESI, EDI et EBP (pas dans cet ordre).

L'instruction POPA peut être utilisée pour les dépiler tous.

V-D. Les Instructions CALL et RET

Le 80x86 fournit deux instructions qui utilisent la pile pour effectuer des appels de sous-programmes rapidement et facilement. L'instruction CALL effectue un saut inconditionnel vers un sous-programme et empile l'adresse de l'instruction suivante. L'instruction RET dépile une adresse et saute à cette adresse. Lors de l'utilisation de ces instructions, il est très important de gérer la pile correctement afin que le chiffre correct soit dépilé par l'instruction RET !

Le programme précédent peut être réécrit pour utiliser ces nouvelles instructions en changeant les lignes 25 à 34 en ce qui suit :

Sélectionnez

mov ebx, input1

call get_int

mov ebx, input2

call get_int

et en changeant le sous-programme get_int en :

Sélectionnez

get_int :

call read_int

mov [ebx], eax

ret

Il y a plusieurs avantages à utiliser CALL et RET :

  • c'est plus simple ! ;
  • cela permet d'imbriquer des appels de sous-programmes facilement.

Notez que get_int appelle read_int. Cet appel empile une autre adresse à la fin du code de read_int se trouve un RET qui dépile l'adresse de retour et saute vers le code de get_int. Puis, lorsque le RET de get_int est exécuté, il dépile l'adresse de retour qui revient vers asm_main. Cela fonctionne correctement, car il s'agit d'une pile LIFO.

Souvenez-vous, il est très important de dépiler toute donnée qui est empilée. Par exemple, considérons le code suivant :

Sélectionnez

1 get_int :

2 call read_int

3 mov [ebx], eax

4 push eax

5 ret ; dépile la valeur de EAX, pas l'adresse de retour !!

Ce code ne reviendra pas correctement !

V-E. Conventions d'appel

Lorsqu'un sous-programme est appelé, le code appelant et le sous-programme (l'appelé) doivent s'accorder sur la façon de se passer les données. Les langages de haut niveau ont des manières standard de passer les données appelées « conventions d'appel ». Pour interfacer du code de haut niveau avec le langage assembleur, le code assembleur doit utiliser les mêmes conventions que le langage de haut niveau. Les conventions d'appel peuvent différer d'un compilateur à l'autre ou peuvent varier selon la façon dont le code est compilé (p.e. selon que les optimisations sont activées ou pas). Une convention universelle est que le code est appelé par une instruction CALL et revient par un RET.

Tous les compilateurs C PC supportent une convention d'appel qui sera décrite dans le reste de ce chapitre par étape. Ces conventions permettent de créer des sous-programmes réentrants. Un sous-programme réentrant peut être appelé depuis n'importe quel endroit du programme en toute sécurité (même depuis le sous-programme lui-même).

V-E-1. Passer les paramètres via la pile

Les paramètres d'un sous-programme peuvent être passés par la pile. Ils sont empilés avant l'instruction CALL. Comme en C, si le paramètre doit être modifié par le sous-programme, l'adresse de la donnée doit être passée, pas sa valeur. Si la taille du paramètre est inférieure à un double-mot, il doit être converti en un double-mot avant d'être empilé.

Les paramètres sur la pile ne sont pas dépilés par le sous-programme, à place, ils sont accédés depuis la pile elle-même. Pourquoi ? Comme ils doivent être empilés avant l'instruction CALL, l'adresse de retour devrait être dépilée avant tout (puis réempilée).

Souvent, les paramètres sont utilisés à plusieurs endroits dans le sous-programme. Habituellement, ils ne peuvent pas être conservés dans un registre durant toute la durée du sous-programme et devront être stockés en mémoire. Les laisser sur la pile conserve une copie de la donnée en mémoire qui peut être accédée depuis n'importe quel endroit du sous-programme.

Considérons un sous-programme auquel on passe un paramètre unique via la pile. Lorsque le sous-programme est appelé, la pile ressemble à la Figure 4.1. On peut accéder au paramètre en utilisant l'adressage indirect ([ESP+4] (20)).

Image non disponible

Lors de l'utilisation de l'adressage indirect, le processeur 80x86 accède à différents segments selon les registres utilisés dans l'expression d'adressage indirect. ESP (et EBP) utilisent le segment de pile alors que EAX, EBX, ECX et EDX utilisent le segment de données.
Cependant, ce n'est habituellement pas important pour la plupart des programmes en mode protégé, car pour eux, les segments de pile et de données sont les mêmes.

Si la pile est également utilisée dans le sous-programme pour stocker des données, le nombre à ajouter à ESP changera. Par exemple, la Figure 4.2 montre à quoi ressemble la pile si un DWORD est empilé.

Image non disponible

Maintenant, le paramètre se trouve en ESP + 8, plus en ESP + 4. Donc, utiliser ESP lorsque l'on fait référence à des paramètres peut être une source d'erreurs. Pour résoudre ce problème, le 80386 fournit un autre registre à utiliser : EBP. La seule utilité de ce registre est de faire référence à des données sur la pile. La convention d'appel C stipule qu'un sous-programme doit d'abord empiler la valeur de EBP puis définir EBP pour qu'il soit égal à ESP. Cela permet à ESP de changer au fur et à mesure que des données sont empilées ou dépilées sans modifier EBP. À la fin du programme, la valeur originale de EBP doit être restaurée (c'est pourquoi elle est sauvegardée au début du sous-programme).

La Figure 4.3 montre la forme générale d'un sous-programme qui suit ces conventions.

Image non disponible

Les lignes 2 et 3 de la Figure 4.3 constituent le prologue générique d'un sous-programme. Les lignes 5 et 6 constituent l'épilogue. La Figure 4.4 montre à quoi ressemble la pile immédiatement après le prologue.

Image non disponible

Maintenant, le paramètre peut être accédé avec [EBP + 8] depuis n'importe quel endroit du programme sans se soucier de ce qui a été empilé entretemps par le sous-programme.

Une fois le sous-programme terminé, les paramètres qui ont été empilés doivent être retirés. La convention d'appel C spécifie que c'est au code appelant de le faire. D'autres conventions sont différentes. Par exemple, la convention d'appel Pascal spécifie que c'est au sous-programme de retirer les paramètres (Il y a une autre forme de l'instruction RET qui permet de le faire facilement). Quelques compilateurs C supportent cette convention également.

Le mot-clé pascal est utilisé dans le prototype et la définition de la fonction pour indiquer au compilateur d'utiliser cette convention. En fait, la convention stdcall, que les fonctions de l'API C MS Windows utilisent, fonctionne également de cette façon. Quel est son avantage ? Elle est un petit peu plus efficace que la convention C. Pourquoi toutes les fonctions C n'utilisent-elles pas cette convention alors ? En général, le C autorise une fonction à avoir un nombre variable d'arguments (p.e., les fonctions printf et scanf).

Pour ce type de fonction, l'opération consistant à retirer les paramètres de la pile varie d'un appel à l'autre. La convention C permet aux instructions nécessaires à la réalisation de cette opération de varier facilement d'un appel à l'autre. Les conventions Pascal et stdcall rendent cette opération très compliquée. Donc, la convention Pascal (comme le langage Pascal) n'autorise pas ce type de fonction. MS Windows peut utiliser cette convention puisqu'aucune de ses fonctions d'API ne prend un nombre variable d'arguments.

La Figure 4.5 montre comment un sous-programme utilisant la convention d'appel C serait appelé.

Image non disponible

La ligne 3 retire le paramètre de la pile en manipulant directement le pointeur de pile. Une instruction POP pourrait également être utilisée, mais cela nécessiterait le stockage d'un paramètre inutile dans un registre. En fait, dans ce cas particulier, beaucoup de compilateurs utilisent une instruction POP ECX pour retirer le paramètre. Le compilateur utilise un POP plutôt qu'un ADD, car le ADD nécessite plus d'octets pour stocker l'instruction. Cependant, le POP change également la valeur de ECX ! Voici un autre programme exemple avec deux sous-programmes qui utilisent les conventions d'appel C dont nous venons de parler. La ligne 54 (et les autres) montre que plusieurs segments de données et de texte peuvent être déclarés dans un même fichier source. Ils seront combinés en des segments de données et de texte uniques lors de l'édition de liens. Diviser les données et le code en segments séparés permet d'avoir les données d'un sous-programme définies à proximité de celui-ci.

sub3.asm

Sélectionnez

1 %include "asm_io.inc"
2
3  segment .data
4 sum dd 0
5
6  segment .bss
7 input resd 1
8
9 ;
10 ; algorithme en pseudo-code
11 ; i = 1;
12 ; sum = 0;
13 ; while( get_int(i, &input), input != 0 ) {
14 ; sum += input;
15 ; i++;
16 ; }
17 ; print_sum(num);
18 segment .text
19 global _asm_main
20 _asm_main :
21 enter 0,0 ; routine d'initialisation
22 pusha
23
24 mov edx, 1 ; edx est le 'i' du pseudo-code
25 while_loop :
26 push edx ; empile i
27 push dword input ; empile l'adresse de input
28 call get_int
29 add esp, 8 ; dépile i et &input
30
31 mov eax, [input]
32 cmp eax, 0
33 je end_while
34
35 add [sum], eax ; sum += input
36
37 inc edx
38 jmp short while_loop
39
40 end_while:
41 push dword [sum] ; empile la valeur de sum
42 call print_sum
43 pop ecx ; dépile [sum]
44
45 popa
46 leave
47 ret
48
49 ; sous-programme get_int
50 ; Paramètres (dans l'ordre de l'empilement)
51 ; nombre de saisies (en [ebp + 12])
52 ; adresse du mot où stocker la saisie (en [ebp + 8])
53 ; Notes :
54 ; les valeurs de eax et ebx sont détruites
55 segment .data
56 prompt db ") Entrez un nombre entier (0 pour quitter): ", 0
57
58 segment .text
59 get_int:
60 push ebp
61 mov ebp, esp
62
63 mov eax, [ebp + 12]
64 call print_int
65
66 mov eax, prompt
67 call print_string
68
69 call read_int
70 mov ebx, [ebp + 8]
71 mov [ebx], eax ; stocke la saisie en mémoire
72
73 pop ebp
74 ret ; retour à l'appelant
75
76 ; sous-programme print_sum
77 ; affiche la somme
78 ; Paramètre :
79 ; somme à afficher (en [ebp+8])
80 ; Note : détruit la valeur de eax
81 ;
82 segment .data
83 result db "La somme vaut ", 0
84
85 segment .text
86 print_sum:
87 push ebp
88 mov ebp, esp
89
90 mov eax, result
91 call print_string
92
93 mov eax, [ebp+8]
94 call print_int
95 call print_nl
96
97 pop ebp
98 ret

V-E-2. Variables locales sur la pile

La pile peut être utilisée comme un endroit pratique pour stocker des variables locales. C'est exactement ce que fait le C pour les variables normales (ou automatiques en C lingo). Utiliser la pile pour les variables est important si l'on veut que les sous-programmes soient réentrants. Un programme réentrant fonctionnera qu'il soit appelé de n'importe quel endroit, même à partir du sous-programme lui-même. En d'autres termes, les sous-programmes réentrants peuvent être appelés récursivement.

Utiliser la pile pour les variables économise également de la mémoire. Les données qui ne sont pas stockées sur la pile utilisent de la mémoire du début à la fin du programme (le C appelle ce type de variables global ou static). Les données stockées sur la pile n'utilisent de la mémoire que lorsque le sous-programme dans lequel elles sont définies est actif.

Les variables locales sont stockées immédiatement après la valeur de EBP sauvegardée dans la pile. Elles sont allouées en soustrayant le nombre d'octets requis de ESP dans le prologue du sous-programme. La Figure 4.6 montre le nouveau squelette du sous-programme.

Image non disponible

Le registre EBP est utilisé pour accéder à des variables locales. Considérons la fonction C de la Figure 4.7.

Image non disponible

La Figure 4.8 montre comment le sous-programme équivalent pourrait être écrit en assembleur.

Image non disponible

La Figure 4.9 montre à quoi ressemble la pile après le prologue du programme de la Figure 4.8.

Image non disponible

Cette section de la pile qui contient les paramètres, les informations de retour et les variables locales est appelée cadre de pile (stack frame). Chaque appel de fonction C crée un nouveau cadre de pile sur la pile.

Le prologue et l'épilogue d'un sous-programme peuvent être simplifiés en utilisant deux instructions spéciales qui sont conçues spécialement dans ce but. L'instruction ENTER effectue le prologue et l'instruction LEAVE l'épilogue.

L'instruction ENTER prend deux opérandes immédiats. Dans la convention d'appel C, le deuxième opérande est toujours 0. Le premier opérande est le nombre d'octets nécessaires pour les variables locales. L'instruction LEAVE n'a pas d'opérande. La Figure 4.10 montre comment ces instructions sont utilisées. Notez que le squelette de programme (Figure 1.7) utilise également ENTER et LEAVE.

Image non disponible

En dépit du fait que ENTER et LEAVE simplifient le prologue et l'épilogue, ils ne sont pas utilisés très souvent. Pourquoi ? Parce qu'ils sont plus lents que les instructions plus simples équivalentes ! C'est un des exemples où il ne faut pas supposer qu'une instruction est plus rapide qu'une séquence de plusieurs instructions.

V-F. Programme multimodule

Un programme multimodule est un programme composé de plus d'un fichier objet. Tous les programmes présentés jusqu'ici sont des programmes multimodules. Ils consistent en un fichier objet C pilote et le fichier objet assembleur (plus les fichiers objet de la bibliothèque C). Souvenez-vous que l'éditeur de liens combine les fichiers objet en un programme exécutable unique. L'éditeur de liens doit rapprocher toutes les références faites à chaque étiquette d'un module (i.e. un fichier objet) de sa définition dans un autre module. Afin que le module A puisse utiliser une étiquette définie dans le module B, la directive extern doit être utilisée. Après la directive extern vient une liste d'étiquettes délimitées par des virgules. La directive indique à l'assembleur de traiter ces étiquettes comme externes au module. C'est-à-dire qu'il s'agit d'étiquettes qui peuvent être utilisées dans ce module, mais sont définies dans un autre. Le fichier asm_io.inc définit les routines read_int, etc. comme externes.

En assembleur, les étiquettes ne peuvent pas être accédées de l'extérieur par défaut. Si une étiquette doit pouvoir être accédée depuis d'autres modules que celui dans lequel elle est dénie, elle doit être déclarée comme globale dans son module, par le biais de la directive global. La ligne 13 du listing du programme squelette de la Figure 1.7 montre que l'étiquette _asm_main est définie comme globale. Sans cette déclaration, l'éditeur de liens indiquerait une erreur. Pourquoi ? Parce que le code C ne pourrait pas faire référence à l'étiquette interne _asm_main.

Voici le code de l'exemple précédent réécrit afin d'utiliser deux modules. Les deux sous-programmes (get_int et print_sum) sont dans des fichiers source distincts de celui de la routine _asm_main.

main4.asm

Sélectionnez

-
1 %include "asm_io.inc"
2
3 segment .data
4 sum dd 0
5
6 segment .bss
7 input resd 1
8
9 segment .text
10 global _asm_main
11 extern get_int, print_sum
12 _asm_main :
13 enter 0,0 ; routine d'initialisation
14 pusha
15
16 mov edx, 1 ; edx est le 'i' du pseudo-code
17 while_loop :
18 push edx ; empile i
19 push dword input ; empile l'adresse de input
20 call get_int
21 add esp, 8 ; dépile i et &input
22
23 mov eax, [input]
24 cmp eax, 0
25 je end_while
26
27 add [sum], eax ; sum += input
28
29 inc edx
30 jmp short while_loop
31
32 end_while:
33 push dword [sum] ; empile la valeur de sum
34 call print_sum
35 pop ecx ; dépile [sum]
36
37 popa
38 leave
39 ret

sub4.asm

Sélectionnez

1 %include "asm_io.inc"
2
3  segment .data
4 prompt db ") Entrez un nombre entier (0 pour quitter): ", 0
5
6 segment .text
7 global get_int, print_sum
8 get_int :
9 enter 0,0
10
11 mov eax, [ebp + 12]
12 call print_int
13
14 mov eax, prompt
15 call print_string
16
17 call read_int
18 mov ebx, [ebp + 8]
19 mov [ebx], eax ; stocke la saisie en mémoire
20
21 leave
22 ret ; retour à l'appelant
23
24 segment .data
25 result db "La somme vaut ", 0
26
27 segment .text
28 print_sum :
29 enter 0,0
30
31 mov eax, result
32 call print_string
33
34 mov eax, [ebp+8]
35 call print_int
36 call print_nl
37
38 leave
39 ret

L'exemple ci-dessus n'a que des étiquettes de code globales ; cependant, les étiquettes de donnée globales fonctionnent exactement de la même façon.

V-G. Interfacer de l'assembleur avec du C

Aujourd'hui, très peu de programmes sont écrits complètement en assembleur.

Les compilateurs sont très performants dans la conversion de code de haut niveau en code machine efficace. Comme il est plus facile d'écrire du code dans un langage de haut niveau, ils sont plus populaires. De plus, le code de haut niveau est beaucoup plus portable que l'assembleur !

Lorsque de l'assembleur est utilisé, c'est souvent pour de petites parties du code. Cela peut être fait de deux façons : en appelant des sous-routines assembleur depuis le C ou en incluant de l'assembleur. Inclure de l'assembleur permet au programmeur de placer des instructions assembleur directement dans le code C. Cela peut être très pratique ; cependant, il y a des inconvénients à inclure l'assembleur. Le code assembleur doit être écrit dans le format que le compilateur utilise. Aucun compilateur pour le moment ne supporte le format NASM. Des compilateurs différents demandent des formats différents. Borland et Microsoft demandent le format MASM. DJGPP et gcc sous Linux demandent le format GAS (21). La technique d'appel d'une sous-routine assembleur est beaucoup plus standardisée sur les PC.

Les routines assembleur sont habituellement utilisées avec le C pour les raisons suivantes :

  • un accès direct aux fonctionnalités matérielles de l'ordinateur est nécessaire, car il est difficile ou impossible d'y accéder en C ;
  • la routine doit être la plus rapide possible et le programmeur peut optimiser le code à la main mieux que le compilateur.

La dernière raison n'est plus aussi valide qu'elle l'était. La technologie des compilateurs a été améliorée au fil des ans et les compilateurs génèrent souvent un code très performant (en particulier si les optimisations sont activées).

Les inconvénients des routines assembleur sont une portabilité et une lisibilité réduites.

La plus grande partie des conventions d'appel C a déjà été présentée.

Cependant, il y a quelques fonctionnalités supplémentaires qui doivent être décrites.

V-G-1. Sauvegarder les registres

Tout d'abord, Le C suppose qu'une sous-routine maintient les valeurs des registres suivants : EBX, ESI, EDI, EBP, CS, DS, SS, ES. Cela ne signifie pas que la sous-routine ne les change pas en interne. Cela signifie que si elle change leurs valeurs, elle doit les restaurer avant de revenir. Les valeurs de EBX, ESI et EDI doivent être inchangées, car le C utilise ces registres pour les variables de registre. Habituellement, la pile est utilisée pour sauvegarder les valeurs originales de ces registres.

Le mot-clé register peut être utilisé dans une déclaration de variable C pour suggérer au compilateur d'utiliser un registre pour cette variable plutôt qu'un emplacement mémoire. On appelle ces variables, variables de registre. Les compilateurs modernes le font automatiquement sans qu'il y ait besoin d'une suggestion.

V-G-2. Étiquettes de fonctions

La plupart des compilateurs C ajoutent un caractère underscore(_) au début des noms des fonctions et des variables global/static. Par exemple, à une fonction appelée f sera assignée l'étiquette _f. Donc, s'il s'agit d'une routine assembleur, elle doit être étiquetée _f, pas f. Le compilateur Linux gcc, n'ajoute aucun caractère. Dans un exécutable Linux ELF, on utiliserait simplement l'étiquette f pour la fonction f. Cependant, le gcc de DJGPP ajoute un underscore. Notez que dans le squelette de programme assembleur (Figure 1.7), l'étiquette de la routine principale est _asm_main.

V-G-3. Passer des paramètres

Dans les conventions d'appel C, les arguments d'une fonction sont empilés sur la pile dans l'ordre inverse de celui dans lequel ils apparaissent dans l'appel de la fonction.

Considérons l'expression C suivante : printf("x = %d\n",x); la Figure 4.11 montre comment elle serait compilée (dans le format NASM équivalent).

Image non disponible

La Figure 4.12 montre à quoi ressemble la pile après le prologue de la fonction printf.

Image non disponible

La fonction printf est une des fonctions de la bibliothèque C qui peut prendre n'importe quel nombre d'arguments. Les règles des conventions d'appel C ont été spécialement écrites pour autoriser ce type de fonctions. Comme l'adresse de la chaîne format est empilée en dernier, son emplacement sur la pile sera toujours EBP + 8 quel que soit le nombre de paramètres passés à la fonction. Le code de printf peut alors analyser la chaîne format pour déterminer combien de paramètres ont dû être passés et les récupérer sur la pile.

Bien sûr, s'il y a une erreur, printf("x = %d\n"), le code de printf affichera quand même la valeur double-mot en [EBP + 12]. Cependant, ce ne sera pas la valeur de x !

Il n'est pas nécessaire d'utiliser l'assembleur pour gérer un nombre aléatoire d'arguments en C. L'en-tête stdarg.h définit des macros qui peuvent être utilisées pour l'effectuer de façon portable. Voyez n'importe quel bon livre sur le C pour plus de détails.

V-G-4. Calculer les adresses des variables locales

Trouver l'adresse d'une étiquette définie dans les segments data ou bss est simple. Basiquement, l'éditeur de liens le fait. Cependant, calculer l'adresse d'une variable locale (ou d'un paramètre) sur la pile n'est pas aussi intuitif.

Néanmoins, c'est un besoin très courant lors de l'appel de sous-routines.

Considérons le cas du passage de l'adresse d'une variable (appelons-la x) à une fonction (appelons-la foo). Si x est situé en EBP − 8 sur la pile, on ne peut pas utiliser simplement :

Sélectionnez

mov eax, ebp – 8

Pourquoi ? La valeur que MOV stocke dans EAX doit être calculée par l'assembleur (c'est-à-dire qu'elle doit donner une constante). Cependant, il y a une instruction qui effectue le calcul désiré. Elle est appelée LEA (pour Load Eective Address, Charger l'Adresse Effective). L'extrait suivant calculerait l'adresse de x et la stockerait dans EAX :

Sélectionnez

lea eax, [ebp - 8]

Maintenant, EAX contient l'adresse de x et peut être placé sur la pile lors de l'appel de la fonction foo. Ne vous méprenez pas, au niveau de la syntaxe, c'est comme si cette instruction lisait la donnée en [EBP−8]; cependant, ce n'est pas vrai. L'instruction LEA ne lit jamais la mémoire ! Elle calcule simplement l'adresse qui sera lue par une autre instruction et stocke cette adresse dans son premier opérande registre. Comme elle ne lit pas la mémoire, aucune taille mémoire (p.e. dword) n'est nécessaire ni autorisée.

V-G-5. Retourner des valeurs

Les fonctions C non void retournent une valeur. Les conventions d'appel C spécifient comment cela doit être fait. Les valeurs de retour sont passées via les registres. Tous les types entiers (char, int, enum, etc.) sont retournés dans le registre EAX. S'ils sont plus petits que 32 bits, ils sont étendus à 32 bits lors du stockage dans EAX (la façon dont ils sont étendus dépend du fait qu'ils sont signés ou non). Les valeurs 64 bits sont retournées dans la paire de registres EDX:EAX. Les valeurs de pointeurs sont également stockées dans EAX. Les valeurs en virgule flottante sont stockées dans le registre STP du coprocesseur arithmétique (ce registre est décrit dans le

chapitre sur les nombres en virgule flottante).

V-G-6. Autres conventions d'appel

Les règles ci-dessus décrivent les conventions d'appel C supportées par tous les compilateurs C 80x86. Souvent, les compilateurs supportent également d'autres conventions d'appel. Lorsqu'il y a une interface avec le langage assembleur, il est très important de connaître les conventions utilisées par le compilateur lorsqu'il appelle votre fonction. Habituellement, par défaut, ce sont les conventions d'appel standard qui sont utilisées, cependant, ce n'est pas toujours le cas (22). Les compilateurs qui utilisent plusieurs conventions ont souvent des options de ligne de commande qui peuvent être utilisées pour changer la convention par défaut. Ils fournissent également des extensions à la syntaxe C pour assigner explicitement des conventions d'appel à des fonctions de manière individuelle. Cependant, ces extensions ne sont pas standardisées et peuvent varier d'un compilateur à l'autre.

Le compilateur GCC autorise différentes conventions d'appel. La convention utilisée par une fonction peut être déclarée explicitement en utilisant l'extension __attribute__ . Par exemple, pour déclarer une fonction void qui utilise la convention d'appel standard appelée f qui ne prend qu'un paramètre int, utilisez la syntaxe suivante pour son prototype :

Sélectionnez

void f ( int ) __attribute__((cdecl));

GCC supporte également la convention d'appel standard call . La fonction ci-dessus pourrait être déclarée afin d'utiliser cette convention en remplaçant le cdecl par stdcall. La différence entre stdcall et cdecl est que stdcall impose à la sous-routine de retirer les paramètres de la pile (comme le fait la convention d'appel Pascal). Donc, la convention stdcall ne peut être utilisée que par des fonctions qui prennent un nombre fixe d'arguments (i.e. celles qui ne sont pas comme printf et scanf).

GCC supporte également un attribut supplémentaire appelé regparm qui indique au compilateur d'utiliser les registres pour passer jusqu'à trois arguments entiers à une fonction au lieu d'utiliser la pile. C'est un type d'optimisation courant que beaucoup de compilateurs supportent.

Borland et Microsoft utilisent une syntaxe commune pour déclarer les conventions d'appel. Ils ajoutent les mots-clés __cdecl et __stdcall au C. Ces mots-clés se comportent comme des modicateurs de fonction et apparaissent immédiatement avant le nom de la fonction dans un prototype.

Par exemple, la fonction f ci-dessus serait définie comme suit par Borland et Microsoft :

Sélectionnez

void __cdecl f( int );

Il y a des avantages et des inconvénients à chacune des conventions d'appel.

Le principal avantage de cdecl est qu'elle est simple et très flexible.

Elle peut être utilisée pour n'importe quel type de fonction C et sur n'importe quel compilateur C. Utiliser d'autres conventions peut limiter la portabilité de la sous-routine. Son principal inconvénient est qu'elle peut être plus lente que certaines autres et utilise plus de mémoire (puisque chaque appel de fonction nécessite du code pour retirer les paramètres de la pile).

L'avantage de la convention stdcall est qu'elle utilise moins de mémoire que cdecl. Aucun nettoyage de pile n'est requis après l'instruction CALL. Son principal inconvénient est qu'elle ne peut pas être utilisée avec des fonctions qui ont un nombre variable d'arguments.

L'avantage d'utiliser une convention qui se sert des registres pour passer des paramètres entiers est la rapidité. Le principal inconvénient est que la convention est plus complexe. Certains paramètres peuvent se trouver dans des registres et d'autres sur la pile.

V-G-7. Exemples

Voici un exemple qui montre comment une routine assembleur peut être interfacée avec un programme C (notez que ce programme n'utilise pas le programme assembleur squelette (Figure 1.7) ni le module driver.c).

main5.c

Sélectionnez

1 #include <stdio.h>
2 / prototype de la routine assembleur /
3 void calc_sum( int, int ) __attribute__((cdecl));
4
5 int main( void )
6 {
7 int n, sum;
8
9 printf ("Somme des entiers jusquà ' : ");
10 scanf("%d", &n);
11 calc_sum(n, &sum);
12 printf ("La somme vaut %d\n", sum);
13 return 0;
14 }

sub5.asm

Sélectionnez

1 ; sous-routine _calc_sum
2 ; trouve la somme des entiers de 1 à n
3 ; Paramètres :
4 ; n - jusqu'où faire la somme (en [ebp + 8])
5 ; sump - pointeur vers un entier dans lequel stocker la somme (en [ebp + 12])
6 ; pseudo-code C :
7 ; void calc_sum( int n, int * sump )
8 ; {
9 ; int i, sum = 0;
10 ; for( i=1; i <= n; i++ )
11 ; sum += i;
12 ; *sump = sum;
13 ; }
14
15 segment .text
16 global _calc_sum
17 ;
18 ; variable locale :
19 ; sum en [ebp-4]
20 _calc_sum :
21 enter 4,0 ; Fait de la place pour sum sur la pile
22 push ebx ; IMPORTANT !
23
24 mov dword [ebp-4],0 ; sum = 0
25 dump_stack 1, 2, 4 ; affiche la pile de ebp-8 à ebp+16
26 mov ecx, 1 ; ecx est le i du pseudocode
27 for_loop:
28 cmp ecx, [ebp+8] ; cmp i et n
29 jnle end_for ; si non i <= n, quitter
30
31 add [ebp-4], ecx ; sum += i
32 inc ecx
33 jmp short for_loop
34
35 end_for:
36 mov ebx, [ebp+12] ; ebx = sump
37 mov eax, [ebp-4] ; eax = sum
38 mov [ebx], eax
39
40 pop ebx ; restaure ebx
41 leave
42 ret

Pourquoi la ligne 22 de sub5.asm est si importante ? Parce que les conventions d'appel C imposent que la valeur de EBX ne soit pas modifiée par l'appel de fonction. Si ce n'est pas respecté, il est très probable que le programme ne fonctionne pas correctement.

La ligne 25 montre la façon dont fonctionne la macro dump_stack. Souvenez-vous que le premier paramètre est juste une étiquette numérique et les deuxième et troisième paramètres déterminent respectivement combien de doubles-mots elle doit afficher en dessous et au-dessus de EBP. La Figure 4.13 montre une exécution possible du programme.

Image non disponible

Pour cette capture, on peut voir que l'adresse du dword où stocker la somme est BFFFFB80 (en EBP + 12); le nombre jusqu'auquel additionner est 0000000A (en EBP + 8), l'adresse de retour pour la routine est 08048501 (en EBP + 4), la valeur sauvegardée de EBP est BFFFFB88 (en EBP), la valeur de la variable locale est 0 en (EBP - 4) et pour finir, la valeur sauvegardée de EBX est 4010648C (en EBP - 8).

La fonction calc_sum pourrait être réécrite pour retourner la somme plutôt que d'utiliser un pointeur. Comme la somme est une valeur entière, elle doit être placée dans le registre EAX. La ligne 11 du fichier main5.c deviendrait :

Sélectionnez

sum = calc_sum(n);

De plus, le prototype de calc_sum devrait être altéré. Voici le code assembleur modifié :

sub6.asm

Sélectionnez

1 ; sous-routine _calc_sum
2 ; trouve la somme des entiers de 1 à n
3 ; Paramètres :
4 ; n - jusqu'où faire la somme (en [ebp + 8])
5 ; Valeur de retour :
6 ; valeur de la somme
7 ; pseudo-code C :
8 ; int calc_sum( int n )
9 ; {
10 ; int i, sum = 0;
11 ; for( i=1; i <= n; i++ )
12 ; sum += i;
13 ; return sum;
14 ; }
15 segment .text
16 global _calc_sum
17 ;
18 ; variable locale :
19 ; sum en [ebp-4]
20 _calc_sum :
21 enter 4,0 ; fait de la place pour la somme sur la pile
22
23 mov dword [ebp-4],0 ; sum = 0
24 mov ecx, 1 ; ecx est le i du pseudocode
25 for_loop:
26 cmp ecx, [ebp+8] ; cmp i et n
27 jnle end_for ; si non i <= n, quitter
28
29 add [ebp-4], ecx ; sum += i
30 inc ecx
31 jmp short for_loop
32
33 end_for:
34 mov eax, [ebp-4] ; eax = sum
35
36 leave
37 ret

V-G-8. Appeler des fonctions C depuis l'assembleur

Un des avantages majeurs d'interfacer le C et l'assembleur est que cela permet au code assembleur d'accéder à la grande bibliothèque C et aux fonctions utilisateur. Par exemple, si l'on veut appeler la fonction scanf pour lire un entier depuis le clavier. La Figure 4.14 montre comment le faire.

Image non disponible

Une chose très importante à se rappeler est que scanf suit les conventions d'appel C standard à la lettre. Cela signifie qu'elle préserve les valeurs des registres EBX, ESI et EDI, cependant, les registres EAX, ECX et EDX peuvent être modifiés ! En fait, EAX sera modifié, car il contiendra la valeur de retour de l'appel à scanf. Pour d'autres exemples d'interface entre l'assembleur et le C, observez le code dans asm_io.asm qui a été utilisé pour créer asm_io.obj.

V-H. Sous-programmes réentrants et récursifs

Un sous-programme réentrant remplit les critères suivants :

  • il ne doit pas modifier son code. Dans un langage de haut niveau, cela serait difficile, mais en assembleur, il n'est pas si dur que cela pour un programme de modifier son propre code. Par exemple :

Sélectionnez

mov word [cs :$+7], 5 ; copie 5 dans le mot 7 octets plus loin

add ax, 2 ; l'expression précédente change 2 en 5 !
  • Il ne doit pas modifier de données globales (comme celles qui se trouvent dans les segments data et bss). Toutes les variables sont stockées sur la pile.

Il y a plusieurs avantages à écrire du code réentrant :

  • un sous-programme réentrant peut être appelé récursivement ;
  • un programme réentrant peut être partagé par plusieurs processus ;
  • sur beaucoup de systèmes d'exploitation multitâches, s'il y a plusieurs instances d'un programme en cours, seule une copie du code se trouve en mémoire. Les bibliothèques partagées et les DLL (Dynamic Link Libraries, Bibliothèques de Lien Dynamique) utilisent le même principe ;
  • les sous-programmes réentrants fonctionnent beaucoup mieux dans les programmes multithreadés (23). Windows 9x/NT et la plupart des systèmes d'exploitation de style Unix (Solaris, Linux, etc.) supportent les programmes multithreadés.

V-H-1. Sous-programmes récursifs

Ce type de sous-programmes s'appellent eux-mêmes. La récursivité peut être soit directe soit indirecte. La récursivité directe survient lorsqu'un sous-programme, disons foo, s'appelle lui-même dans le corps de foo. La récursivité indirecte survient lorsqu'un sous-programme ne s'appelle pas directement lui-même, mais via un autre sous-programme qu'il appelle. Par exemple, le sous-programme foo pourrait appeler bar et bar pourrait appeler foo.

Les sous-programmes récursifs doivent avoir une condition de terminaison.

Lorsque cette condition est vraie, il n'y a plus d'appel récursif. Si une routine récursive n'a pas de condition de terminaison ou que la condition n'est jamais remplie, la récursivité ne s'arrêtera jamais (exactement comme une boucle infinie).

La Figure 4.15 montre une fonction qui calcule une factorielle récursivement.

Elle peut être appelée depuis le C avec :

Sélectionnez

x = fact (3); / trouve 3! /

Image non disponible

La Figure 4.16 montre à quoi ressemble la pile au point le plus profond pour l'appel de fonction ci-dessus.

Image non disponible

Les Figures 4.17 et 4.18 montrent un exemple récursif plus compliqué en C et en assembleur, respectivement.

Image non disponible Image non disponible

Quelle est la sortie pour f(3) ? Notez que l'instruction ENTER crée un nouveau i sur la pile pour chaque appel récursif.

Donc, chaque instance récursive de f a sa propre variable i indépendante.

Définir i comme un double-mot dans le segment data ne fonctionnerait pas pareil.

V-H-2. Révision des types de stockage des variables en C

Le C fournit plusieurs types de stockage des variables :

global : ces variables sont déclarées en dehors de toute fonction et sont stockées à des emplacements mémoire fixes (dans les segments data ou bss) et existent depuis le début du programme jusqu'à la fin. Par défaut, on peut y accéder de n'importe quelle fonction dans le programme, cependant, si elles sont déclarées comme static, seules les fonctions dans le même module peuvent y accéder (i.e. en termes assembleur, l'étiquette est interne, pas externe) ;

static : il s'agit des variables locales d'une fonction qui sont déclarées static (malheureusement, le C utilise le mot-clé static avec deux sens différents !). Ces variables sont également stockées dans des emplacements mémoire fixes (dans data ou bss), mais ne peuvent être accédées directement que dans la fonction où elles sont définies ;

automatic : c'est le type par défaut d'une variable C dénie dans une fonction. Ces variables sont allouées sur la pile lorsque la fonction dans laquelle elles sont définies est appelée et sont désallouées lorsque la fonction revient. Donc, elles n'ont pas d'emplacement mémoire fixe.

register ; ce mot-clé demande au compilateur d'utiliser un registre pour la donnée dans cette variable. Il ne s'agit que d'une requête. Le compilateur n'a pas à l'honorer. Si l'adresse de la variable est utilisée à un endroit quelconque du programme, elle ne sera pas respectée (puisque les registres n'ont pas d'adresse). De plus, seuls les types entiers simples peuvent être des valeurs registres. Les types structures ne peuvent pas l'être : ils ne tiendraient pas dans un registre ! Les compilateurs C placent souvent les variables automatiques normales dans des registres sans aide du programmeur.

volatile : ce mot-clé indique au compilateur que la valeur de la variable peut changer à tout moment. Cela signifie que le compilateur ne peut faire aucune supposition sur le moment où la variable est modifiée. Souvent, un compilateur stocke la valeur d'une variable dans un registre temporairement et utilise le registre à la place de la variable dans une section de code. Il ne peut pas faire ce type d'optimisations avec les variables volatiles. Un exemple courant de variable volatile serait une variable qui peut être modifiée par deux threads d'un programme multithreadé. Considérons le code suivant :

Sélectionnez

1 x = 10;

2 y = 20;

3 z = x;

Si x pouvait être altéré par un autre thread, il est possible que l'autre thread change x entre les lignes 1 et 3, alors z ne vaudrait pas 10.

Cependant, si x n'a pas été déclaré comme volatile, le compilateur peut supposer que x est inchangé et positionner z à 10.

Une autre utilisation de volatile est d'empêcher le compilateur d'utiliser un registre pour une variable.

VI. Tableaux

VI-A. Introduction

Un tableau est un bloc contigu de données en mémoire. Tous les éléments de la liste doivent être du même type et occuper exactement le même nombre d'octets en mémoire. En raison de ces propriétés, les tableaux permettent un accès efficace à une donnée par sa position (ou indice) dans le tableau.

L'adresse de n'importe quel élément peut être calculée en connaissant les trois choses suivantes :

  • l'adresse du premier élément du tableau ;
  • le nombre d'octets de chaque élément ;
  • l'indice de l'élément.

Il est pratique de considérer l'indice du premier élément du tableau comme étant 0 (comme en C). Il est possible d'utiliser d'autres valeurs pour le premier indice, mais cela complique les calculs.

VI-A-1. Définir des tableaux

VI-A-1-a. Définir des tableaux dans les segments data et bss

Pour définir un tableau initialisé dans le segment data, utilisez les directives db, dw, etc. normales. directives. NASM fournit également une directive utile appelée TIMES qui peut être utilisée pour répéter une expression de nombreuses fois sans avoir à la dupliquer à la main. La Figure 5.1 montre plusieurs exemples.

Image non disponible

Pour définir un tableau non initialisé dans le segment bss, utilisez les directives resb, resw, etc. Souvenez-vous que ces directives ont un opérande qui spécifie le nombre d'unités mémoire à réserver. La Figure 5.1 montre également des exemples de ces types de définitions.

VI-A-1-b. Définir des tableaux comme variables locales

Il n'y a pas de manière directe de définir un tableau comme variable locale sur la pile. Comme précédemment, on calcule le nombre total d'octets requis pour toutes les variables locales, y compris les tableaux, et on l'ôte de ESP (soit directement, soit en utilisant l'instruction ENTER). Par exemple, si une fonction a besoin d'une variable caractère, deux entiers double-mot et un tableau de 50 éléments d'un mot, il faudrait 1+2×4+50×2 = 109 octets.

Cependant, le nombre ôté de ESP doit être un multiple de quatre (112 dans ce cas) pour maintenir ESP sur un multiple de double-mot. On peut organiser les variables dans ces 109 octets de plusieurs façons. La Figure 5.2 montre deux manières possibles.

Image non disponible

La partie inutilisée sert à maintenir les doubles-mots sur des adresses multiples de doubles-mots afin d'accélérer les accès mémoire.

VI-A-2. Accéder aux éléments de tableaux

Il n'y a pas d'opérateur [ ] en langage assembleur comme en C. Pour accéder à un élément d'un tableau, son adresse doit être calculée. Considérons les deux définitions de tableau suivantes :

Sélectionnez

array1 db 5, 4, 3, 2, 1 ; tableau d'octets

array2 dw 5, 4, 3, 2, 1 ; tableau de mots

Voici quelques exemples utilisant ces tableaux :

Sélectionnez

1 mov al, [array1] ; al = array1[0]

2 mov al, [array1 + 1] ; al = array1[1]

3 mov [array1 + 3], al ; array1[3] = al

4 mov ax, [array2] ; ax = array2[0]

5 mov ax, [array2 + 2] ; ax = array2[1] (PAS array2[2]!)

6 mov [array2 + 6], ax ; array2[3] = ax

7 mov ax, [array2 + 1] ; ax = ??

À la ligne ProductID="5, l" w:st="on">5, l'élément 1 du tableau de mots est référencé, pas l'élément 2.

Pourquoi ? Les mots sont des unités de deux octets, donc pour se déplacer à l'élément suivant d'un tableau de mots, on doit se déplacer de deux octets, pas d'un seul.

La ligne 7 lit un octet du premier élément et un octet du suivant. En C, le compilateur regarde le type de pointeur pour déterminer de combien d'octets il doit se déplacer dans une expression utilisant l'arithmétique des pointeurs, afin que le programmeur n'ait pas à le faire.

Cependant, en assembleur, c'est au programmeur de prendre en compte la taille des éléments du tableau lorsqu'il se déplace parmi les éléments.

La Figure 5.3 montre un extrait de code qui additionne tous les éléments de array1 de l'exemple de code précédent.

Image non disponible

À la ligne 7, AX est ajouté à DX. Pourquoi pas AL ? Premièrement, les deux opérandes de l'instruction ADD doivent avoir la même taille. Deuxièmement, il serait facile d'ajouter des octets et d'obtenir une somme qui serait trop grande pour tenir sur un octet. En utilisant DX, la somme peut atteindre 65 535. Cependant, il est important de réaliser que AH est également additionné. C'est pourquoi, AH est positionné à zéro (24) à la ligne 3.

Les Figures 5.4 et 5.5 montrent deux manières alternatives de calculer la somme. Les lignes en italiques remplacent les lignes 6 et 7 de la Figure 5.3.

Image non disponible Image non disponible

VI-A-3. Adressage indirect plus avancé

Ce n'est pas étonnant, l'adressage indirect est souvent utilisé avec les tableaux. La forme la plus générale d'une référence mémoire indirecte est :

Sélectionnez

[ reg de base + facteur *reg d'index + constante ]

où :

reg de base est un des registres EAX, EBX, ECX, EDX, EBP, ESP, ESI ou EDI ;

facteur est 1, 2, 4 ou 8 (s'il vaut 1, le facteur est omis) ;

reg d'index est un des registres EAX, EBX, ECX, EDX, EBP, ESI, EDI (notez que ESP n'est pas dans la liste) ;

constante est une constante 32 bits. Cela peut être une étiquette (ou une expression d'étiquette).

VI-A-4. Exemple

Voici un exemple qui utilise un tableau et le passe à une fonction. Il utilise le programme array1c.c (dont le listing suit) comme pilote, pas le programme driver.c.

array1.asm

Sélectionnez

1 ?fine ARRAY_SIZE 100
2 ?fine NEW_LINE 10
3
4 segment .data
5 FirstMsg db "10 premiers éléments du tableau", 0
6 Prompt db "Entrez l'indice de l'élément à afficher : ", 0
7 SecondMsg db "L'élément %d vaut %d", NEW_LINE, 0
8 ThirdMsg db "Éléments 20 à 29 du tableau", 0
9 InputFormat db "%d", 0
10
11 segment .bss
12 array resd ARRAY_SIZE
13
14 segment .text
15 extern _puts, _printf, _scanf, _dump_line
16 global _asm_main
17 _asm_main :
18 enter 4,0 ; variable locale dword en EBP – 4
19 push ebx
20 push esi
21
22 ; initialise le tableau à 100, 99, 98, 97...
23
24 mov ecx, ARRAY_SIZE
25 mov ebx, array
26 init_loop:
27 mov [ebx], ecx
28 add ebx, 4
29 loop init_loop
30
31 push dword FirstMsg ; affiche FirstMsg
32 call _puts
33 pop ecx
34
35 push dword 10
36 push dword array
37 call _print_array ; affiche les 10 premiers éléments du tableau
38 add esp, 8
39
40 ; demande à l'utilisateur l'indice de l'élément
41 Prompt_loop:
42 push dword Prompt
43 call _printf
44 pop ecx
45
46 lea eax, [ebp-4] ; eax = adresse du dword local
47 push eax
48 push dword InputFormat
49 call _scanf
50 add esp, 8
51 cmp eax, 1 ; eax = valeur de retour de scanf
52 je InputOK
53
54 call _dump_line ; ignore le reste de la ligne et recommence
55 jmp Prompt_loop ; si la saisie est invalide
56
57 InputOK:
58 mov esi, [ebp-4]
59 push dword [array + 4*esi]
60 push esi
61 push dword SecondMsg ; affiche la valeur de l'élément
62 call _printf
63 add esp, 12
64
65 push dword ThirdMsg ; affiche les éléments 20 à 29
66 call _puts
67 pop ecx
68
69 push dword 10
70 push dword array + 20*4 ; adresse de array[20]
71 call _print_array
72 add esp, 8
73
74 pop esi
75 pop ebx
76 mov eax, 0 ; retour au C
77 leave
78 ret
79
80 ;
81 ; routine _print_array
82 ; Routine appelable depuis le C qui affiche les éléments d'un tableau de doubles mots
83 ; comme des entiers signés.
84 ; Prototype C :
85 ; void print_array( const int * a, int n);
86 ; Paramètres :
87 ; a - pointeur vers le tableau à afficher (en ebp+8 sur la pile)
88 ; n - nombre d'entiers à afficher (en ebp+12 sur la pile)
89
90 segment .data
91 OutputFormat db "%-5d ]", NEW_LINE, 0
92
93 segment .text
94 global _print_array
95 _print_array :
96 enter 0,0
97 push esi
98 push ebx
99
100 xor esi, esi ; esi = 0
101 mov ecx, [ebp+12] ; ecx = n
102 mov ebx, [ebp+8] ; ebx = adresse du tableau
103 print_loop :
104 push ecx ; printf change ecx !
105
106 push dword [ebx + 4*esi] ; empile tableau[esi]
107 push esi
108 push dword OutputFormat
109 call _printf
110 add esp, 12 ; retire les paramètres (laisse ecx !)
111
112 inc esi
113 pop ecx
114 loop print_loop
115
116 pop ebx
117 pop esi
118 leave
119 ret

array1c.c

Sélectionnez

1 #include <stdio.h>
2
3  int asm_main( void );
4 void dump_line( void );
5
6 int main()
7 {
8 int ret_status ;
9 ret_status = asm_main();
10 return ret_status ;
11 }
12
13 /
14 fonction dump_line
15 retire tous les caractères restant sur la ligne courante dans le buffer d'entreé
16 /
17 void dump_line()
18 {
19 int ch;
20
21 while( (ch = getchar()) != EOF && ch != '\n')
22 / null body/ ;
23 }

VI-A-4-a. L'instruction LEA revisitée

L'instruction LEA peut être utilisée dans d'autres cas que le calcul d'adresse.

Elle est assez couramment utilisée pour les calculs rapides. Considérons le code suivant :

Sélectionnez

lea ebx, [4*eax + eax]

Il stocke la valeur de 5×EAX dans EBX. Utiliser LEA dans ce cas est à la fois plus simple et plus rapide que d'utiliser MUL. Cependant, il faut être conscient du fait que l'expression entre crochets doit être une adresse indirecte légale.

Donc, par exemple, cette instruction ne peut pas être utilisée pour multiplier par 6 rapidement.

VI-A-5. Tableaux Multidimensionnels

Les tableaux multidimensionnels ne sont pas vraiment différents de ceux à une dimension dont nous avons déjà parlé. En fait, ils sont représentés en mémoire exactement comme cela, un tableau à une dimension.

VI-A-5-a. Tableaux à Deux Dimensions

Ce n'est pas étonnant, le tableau multidimensionnel le plus simple est celui à deux dimensions. Un tableau à deux dimensions est souvent représenté comme une grille d'éléments. Chaque élément est identifié par une paire d'indices. Par convention, le premier indice est associé à la ligne et le second à la colonne.

Considérons un tableau avec trois lignes et deux colonnes défini de la manière suivante :

Sélectionnez

int a [3][2];

Le compilateur C réserverait de la place pour un tableau d'entiers de 6 (= 2 × 3) et placerait les éléments comme suit :

Image non disponible

Ce que ce tableau tente de montrer et que l'élément référencé comme a[0][0] est stocké au début du tableau de 6 éléments à une dimension. L'élément a[0][1] est stocké à la position suivante (indice 1), etc. Chaque ligne du tableau à deux dimensions est stockée en mémoire de façon contigüe. Le dernier élément d'une ligne est suivi par le premier élément de la suivante. On appelle cela la représentation au niveau ligne (rowwise) du tableau et c'est comme cela qu'un compilateur C/C++ représenterait le tableau.

Comment le compilateur détermine où a[i][j] se trouve dans la représentation au niveau ligne ? Une formule simple calcule l'indice à partir de i et j. La formule dans ce cas est 2i + j. Il n'est pas compliqué de voir comment on obtient cette formule. Chaque ligne fait deux éléments de long, donc le premier élément de la ligne i est à l'emplacement 2i. Puis on obtient l'emplacement de la colonne j en ajoutant j à 2i. Cette analyse montre également comment la formule est généralisée à un tableau de N colonnes : N × i + j.

Notez que la formule ne dépend pas du nombre de lignes.

Pour illustrer, voyons comment gcc compile le code suivant (utilisant le tableau a défini plus haut) :

Sélectionnez

x = a[ i ][ j ];

La Figure 5.6 montre le code assembleur correspondant.

Image non disponible

Donc, le compilateur convertit grossièrement le code en :

Sélectionnez

x = (&a[0][0] + 2i + j );

et en fait, le programmeur pourrait l'écrire de cette manière et obtenir le même résultat.

Il n'y a rien de magique à propos du choix de la représentation niveau ligne du tableau. Une représentation niveau colonne fonctionnerait également :

Image non disponible

Dans la représentation niveau colonne, chaque colonne est stockée de manière contigüe. L'élément [i][j] est stocké à l'emplacement i+3j. D'autres langages (FORTRAN, par exemple) utilisent la représentation niveau colonne.

C'est important lorsque l'on interface du code provenant de multiples langages.

VI-A-5-b. Dimensions supérieures à deux

Pour les dimensions supérieures à deux, la même idée de base est appliquée. Considérons un tableau à trois dimensions :

Sélectionnez

int b [4][3][2];

Ce tableau serait stocké comme s'il était composé de trois tableaux à deux dimensions, chacun de taille [3][2] stockés consécutivement en mémoire. Le tableau ci-dessous montre comment il commence :

Image non disponible

La formule pour calculer la position de b[i][j][k] est 6i + 2j + k. Le 6 est déterminé par la taille des tableaux [3][2]. En général, pour un tableau de dimension a[L][M][N] l'emplacement de l'élément a[i][j][k] sera M ×N ×i+N ×j +k. Notez, là encore, que la dimension L n'apparaît pas dans la formule.

Pour les dimensions plus grandes, le même procédé est généralisé. Pour un tableau à n dimensions de dimension D1 à Dn, l'emplacement d'un élément repéré par les indices i1 à in est donné par la formule :

D2 × D3 · · · × Dn × i1 + D3 × D4 · · · × Dn × i2 + · · · + Dn × in−1 + in

ou pour les fanas de maths, on peut l'écrire de façon plus concise :

Image non disponible

La première dimension, D1, n'apparaît pas dans la formule.

Pour la représentation au niveau colonne, la formule générale serait :

i1 +D1 ×i2 +· · ·+D1 ×D2 ×· · ·×Dn−2 ×in−1 +D1 ×D2 ×· · ·×Dn−1 ×in

ou dans la notation des fanas de maths :

Image non disponible

Dans ce cas, c'est la dernière dimension, Dn, qui n'apparaît pas dans la formule.

C'est là que vous comprenez que l'auteur est un major de physique (ou la référence à FORTRAN vous avait déjà mis sur la voie ?)

VI-A-5-c. Passer des tableaux multidimensionnels comme paramètres en C

La représentation au niveau ligne des tableaux multidimensionnels a un effet direct sur la programmation C. Pour les tableaux à une dimension, la taille du tableau n'est pas nécessaire pour calculer l'emplacement en mémoire de n'importe quel élément. Ce n'est pas vrai pour les tableaux multidimensionnels.

Pour accéder aux éléments de ces tableaux, le compilateur doit connaître toutes les dimensions sauf la première. Cela saute aux yeux lorsque l'on observe le prototype d'une fonction qui prend un tableau multidimensionnel comme paramètre. Ce qui suit ne compilera pas :

Sélectionnez

void f ( int a [ ][ ] ); / pas d' information sur la dimension /

Cependant, ce qui suit compile :

Sélectionnez

void f ( int a [ ][2] );

Tout tableau à deux dimensions à deux colonnes peut être passé à cette fonction. La première dimension n'est pas nécessaire (25).

Ne confondez pas avec une fonction ayant ce prototype :

Sélectionnez

void f ( int a [ ] );

Cela définit un tableau à une dimension de pointeurs sur des entiers (qui peut à son tour être utilisé pour créer un tableau de tableaux qui se comportent plus comme un tableau à deux dimensions).

Pour les tableaux de dimensions supérieures, toutes les dimensions sauf la première doivent être données pour les paramètres. Par exemple, un paramètre tableau à quatre dimensions peut être passé de la façon suivante :

Sélectionnez

void f ( int a [ ][4][3][2] );

VI-B. Instructions de tableaux/chaînes

La famille des processeurs 80x86 fournit plusieurs instructions conçues pour travailler avec les tableaux. Ces instructions sont appelées instructions de chaînes. Elles utilisent les registres d'index (ESI et EDI) pour effectuer une opération puis incrémentent ou décrémentent automatiquement l'un des registres d'index ou les deux. Le drapeau de direction (DF) dans le registre FLAGS détermine si les registres d'index sont incrémentés ou décrémentés.

Il y a deux instructions qui modifient le drapeau de direction :

CLD éteint le drapeau de direction. Les registres d'index sont alors incrémentés ;

STD allume le drapeau de direction. Les registres d'index sont alors décrémentés.

Une erreur très courante dans la programmation 80x86 est d'oublier de positionner le drapeau de direction. Cela conduit souvent à un code qui fonctionne la plupart du temps (lorsque le drapeau de direction est dans la position désirée), mais ne fonctionne pas tout le temps.

VI-B-1. Lire et écrire en mémoire

Les instructions de chaînes les plus simples lisent ou écrivent en mémoire, ou les deux. Elles peuvent lire ou écrire un octet, un mot ou un double-mot à la fois. La Figure 5.7 montre ces instructions avec une brève description de ce qu'elles font en pseudo-code.

Image non disponible

Il y a plusieurs choses à noter ici. Tout d'abord, ESI est utilisé pour lire et EDI pour écrire. C'est facile à retenir si l'on se souvient que SI signifie Source Index (Indice Source) et DI Destination Index (Indice de Destination). Ensuite, notez que le registre qui contient la donnée est fixe (AL, AX ou EAX). Enfin, notez que les instructions de stockage utilisent ES pour déterminer le segment dans lequel écrire, pas DS. En programmation en mode protégé, ce n'est habituellement pas un problème, puisqu'il n'y a qu'un segment de données et ES est automatiquement initialisé pour y faire référence (tout comme DS). Cependant, en programmation en mode réel, il est très important pour le programmeur d'initialiser ES avec la valeur de sélecteur de segment correcte (26). La Figure 5.8 montre un exemple de l'utilisation de ces instructions qui copie un tableau dans un autre.

Image non disponible

La combinaison des instructions LODSx et STOSx (comme aux lignes 13 et 14 de la Figure 5.8) est très courante. En fait, cette combinaison peut être effectuée par une seule instruction de chaînes MOVSx. La Figure 5.9 décrit les opérations effectuées par cette instruction.

Image non disponible

Les lignes 13 et 14 de la Figure 5.8 pourraient être remplacées par une seule instruction MOVSD avec le même résultat. La seule différence serait que le registre EAX ne serait pas utilisé du tout dans la boucle.

VI-B-2. Le préfixe d'instruction REP

La famille 80x86 fournit un préfixe d'instruction spécial (27) appelé REP qui peut être utilisé avec les instructions de chaînes présentées ci-dessus. Ce préfixe indique au processeur de répéter l'instruction de chaînes qui suit un nombre précis de fois. Le registre ECX est utilisé pour compter les itérations (exactement comme pour l'instruction LOOP). En utilisant le préfixe REP, la boucle de la Figure 5.8 (lignes 12 à 15) pourrait être remplacée par une seule ligne :

Sélectionnez

rep movsd

La Figure 5.10 montre un autre exemple qui met à zéro le contenu d'un tableau.

Image non disponible

VI-B-3. Instructions de comparaison de chaînes

La Figure 5.11 montre plusieurs nouvelles instructions de chaînes qui peuvent être utilisées pour comparer des données en mémoire entre elles ou avec des registres. Elles sont utiles pour comparer des tableaux ou effectuer des recherches. Elles positionnent le registre FLAGS exactement comme l'instruction CMP. Les instructions CMPSx comparent les emplacements mémoire correspondants et les instructions SCASx scannent des emplacements mémoire pour une valeur particulière.

La Figure 5.12 montre un court extrait de code qui recherche le nombre 12 dans un tableau de doubles-mots.

Image non disponible

L'instruction SCASD à la ligne 10 ajoute toujours 4 à EDI, même si la valeur recherchée est trouvée. Donc, si l'on veut trouver l'adresse du 12 dans le tableau, il est nécessaire de soustraire 4 de EDI (comme le fait la ligne 16).

VI-B-4. Les préfixes d'instruction REPx

Il y a plusieurs autres préfixes d'instruction du même genre que REP qui peuvent être utilisés avec les instructions de comparaison de chaînes.

La Figure 5.13 montre les deux nouveaux préfixes et décrit ce qu'ils font.

Image non disponible

REPE et REPZ sont simplement des synonymes pour le même préfixe (comme le sont REPNE et REPNZ). Si l'instruction de comparaison de chaînes répétée stoppe à cause du résultat de la comparaison, le ou les registres d'index sont quand même incrémentés et ECX décrémenté, cependant, le registre FLAGS contient toujours l'état qu'il avait à la fin de la répétition. Donc, il est possible d'utiliser le drapeau Z pour déterminer si la comparaison s'est arrêtée à cause de son résultat ou si c'est parce que ECX a atteint zéro.

Pourquoi ne peut-on pas simplement regarder si ECX est à zéro après la comparaison répétée ?

La Figure 5.14 montre un extrait de code qui détermine si deux blocs de mémoire sont égaux.

Image non disponible

Le JE de la ligne 7 de l'exemple vérifie le résultat de l'instruction précédente. Si la comparaison s'est arrêtée parce qu'elle a trouvé deux octets différents, le drapeau Z sera toujours éteint et aucun branchement n'est effectué, cependant, si la comparaison s'est arrêtée parce que ECX a atteint zéro, le drapeau Z sera toujours allumé et le code se branche à l'étiquette equal.

VI-B-5. Exemple

Cette section contient un fichier source assembleur avec plusieurs fonctions qui implémentent des opérations sur les tableaux en utilisant des instructions de chaînes. Beaucoup de ces fonctions font doublons avec des fonctions familières de la bibliothèque C.

memory.asm

Sélectionnez

1 global _asm_copy, _asm_find, _asm_strlen, _asm_strcpy
2
3 segment .text
4 ; fonction _asm_copy
5 ; copie deux blocs de mémoire
6 ; prototype C
7 ; void asm_copy( void * dest, const void * src, unsigned sz);
8 ; paramètres :
9 ; dest - pointeur sur le tampon vers lequel copier
10 ; src - pointeur sur le tampon depuis lequel copier
11 ; sz - nombre d'octets à copier
12
13 ; ci-dessous, quelques symboles utiles sont définis
14
15 ?fine dest [ebp+8]
16 ?fine src [ebp+12]
17 ?fine sz [ebp+16]
18 _asm_copy :
19 enter 0, 0
20 push esi
21 push edi
22
23 mov esi, src ; esi = adresse du tampon depuis lequel copier
24 mov edi, dest ; edi = adresse du tampon vers lequel copier
25 mov ecx, sz ; ecx = nombre d'octets à copier
26
27 cld ; éteint le drapeau de direction
28 rep movsb ; exécute movsb ECX fois
29
30 pop edi
31 pop esi
32 leave
33 ret
34
35
36 ; fonction _asm_find
37 ; recherche un octet donné en mémoire
38 ; void * asm_find( const void * src, char target, unsigned sz);
39 ; paramètres :
40 ; src - pointeur sur le tampon dans lequel chercher
41 ; target - octet à rechercher
42 ; sz - nombre d'octets dans le tampon
43 ; valeur de retour :
44 ; si l'élément recherché est trouvé, un pointeur vers sa première occurrence dans le tampon
45 ; est retourné
46 ; sinon, NULL est retourné
47 ; NOTE : l'élément à rechercher est un octet, mais il est empilé comme une valeur dword.
48 ; La valeur de l'octet est stockée dans les 8 bits de poids faible.
49 ;
50 ?fine src [ebp+8]
51 ?fine target [ebp+12]
52 ?fine sz [ebp+16]
53
54 _asm_find :
55 enter 0,0
56 push edi
57
58 mov eax, target ; al a la valeur recherchée
59 mov edi, src
60 mov ecx, sz
61 cld
62
63 repne scasb ; scanne jusqu'à ce que ECX == 0 ou [ES:EDI] == AL
64
65 je found_it ; si le drapeau zéro est allumé, on a trouvé
66 mov eax, 0 ; si pas trouvé, retourner un pointeur NULL
67 jmp short quit
68 found_it :
69 mov eax, edi
70 dec eax ; si trouvé retourner (DI - 1)
71 quit :
72 pop edi
73 leave
74 ret
75
76
77 ; fonction _asm_strlen
78 ; retourne la taille d'une chaîne
79 ; unsigned asm_strlen( const char * );
80 ; paramètre :
81 ; src - pointeur sur la chaîne
82 ; valeur de retour :
83 ; nombre de caractères dans la chaîne (sans compter le 0 terminal) (dans EAX)
84
85 ?fine src [ebp + 8]
86 _asm_strlen :
87 enter 0,0
88 push edi
89
90 mov edi, src ; edi = pointeur sur la chaîne
91 mov ecx, 0FFFFFFFFh ; utilise la plus grande valeur possible de ECX
92 xor al,al ; al = 0
93 cld
94
95 repnz scasb ; recherche le 0 terminal
96
97 ;
98 ; repnz ira un cran trop loin, donc la longueur vaut FFFFFFFE - ECX,
99 ; pas FFFFFFFF - ECX
100 ;
101 mov eax,0FFFFFFFEh
102 sub eax, ecx ; longueur = 0FFFFFFFEh - ecx
103
104 pop edi
105 leave
106 ret
107
108 ; fonction _asm_strcpy
109 ; copie une chaîne
110 ; void asm_strcpy( char * dest, const char * src);
111 ; paramètres :
112 ; dest - pointeur sur la chaine vers laquelle copier
113 ; src - pointeur sur la chaîne depuis laquelle copier
114 ;
115 ?fine dest [ebp + 8]
116 ?fine src [ebp + 12]
117 _asm_strcpy :
118 enter 0,0
119 push esi
120 push edi
121
122 mov edi, dest
123 mov esi, src
124 cld
125 cpy_loop:
126 lodsb ; charge AL & incrémente SI
127 stosb ; stocke AL & incrémente DI
128 or al, al ; positionne les drapeaux de condition
129 jnz cpy_loop ; si l'on n’est pas après le 0 terminal, on continue
130
131 pop edi
132 pop esi
133 leave
134 ret

memex.c

Sélectionnez

1 #include <stdio.h>
2
3 #dene STR_SIZE 30
4 / prototypes /
5
6 void asm_copy( void , const void , unsigned ) __attribute__((cdecl));
7 void asm_nd( const void ,
8 char target , unsigned ) __attribute__((cdecl));
9 unsigned asm_strlen( const char ) __attribute__((cdecl));
10 void asm_strcpy( char , const char ) __attribute__((cdecl));
11
12 int main()
13 {
14 char st1 [STR_SIZE] = "chaine test";
15 char st2 [STR_SIZE];
16 char st ;
17 char ch;
18
19 asm_copy(st2, st1, STR_SIZE); / copie les 30 caractères de la chaine /
20 printf ("%s\n", st2);
21
22 printf ("Entrez un è caractre : " ); / recherche un octet dans la îchane /
23 scanf("%c%[^\n]", &ch);
ProductID="24 st" w:st="on">24 st = asm_nd(st2, ch, STR_SIZE);
25 if ( st )
26 printf ("éTrouv : %s\n", st );
27 else
28 printf ("Pas étrouv\n");
29
30 st1 [0] = 0;
31 printf ("Entrez une îchane :");
32 scanf("%s", st1);
33 printf ("longueur = %u\n", asm_strlen(st1));
34
35 asm_strcpy( st2, st1 ); / copie des édonnes dans la îchane /
36 printf ("%s\n", st2 );
37
38 return 0;
39 }

VII. Virgule flottante

VII-A. Représentation en virgule flottante

VII-A-1. Nombres binaires non entiers

Lorsque nous avons parlé des systèmes numériques dans le premier chapitre, nous n'avons abordé que les nombres entiers. Évidemment, il est possible de représenter des nombres non entiers dans des bases autres que le décimal.

En décimal, les chiffres à droite du symbole décimal sont associés à des puissances

négatives de 10 :

0, 123 = 1 × 10-1 + 2 × 10-2 + 3 × 10-3

Ce n'est pas étonnant, les nombres binaires fonctionnent de la même façon :

0, 1012 = 1 × 2-1 + 0 × 2-2 + 1 × 2-3 = 0, 625

Cette idée peut être associée avec les méthodes appliquées aux entiers dans le Chapitre II pour convertir un nombre quelconque :

110, 0112 = 4 + 2 + 0, 25 + 0, 125 = 6, 375

Convertir du binaire vers le décimal n'est pas très difficile non plus. Divisez le nombre en deux parties : entière et décimale. Convertissez la partie entière en binaire en utilisant les méthodes du Chapitre II, la partie décimale est convertie en utilisant la méthode décrite ci-dessous.

Considérons une partie décimale binaire dont les bits sont notés a, b, c… Le nombre en binaire ressemble alors à :

0, abcdef…

Multipliez le nombre par deux. La représentation du nouveau nombre sera :

a, bcdef…

Notez que le premier bit est maintenant en tête. Remplacez le a par 0 pour obtenir :

0, bcdef…

et multipliez à nouveau par deux, vous obtenez :

b, cdef…

Maintenant le deuxième bit (b) est à la première place. Cette procédure peut être répétée jusqu'à ce qu'autant de bits que nécessaire soient trouvés.

La Figure 6.1 montre un exemple réel qui converti 0,5625 en binaire.

Image non disponible

La méthode s'arrête lorsque la partie décimale arrive à zéro.

Prenons un autre exemple, considérons la conversion de 23,85 en binaire, il est facile de convertir la partie entière (23 = 101112), mais qu'en est-il de la partie décimale (0, 85) ?

La Figure 6.2 montre le début de ce calcul.

Image non disponible Image non disponible

Une conséquence importante du calcul ci-dessus est que 23,85 ne peut pas être représenté exactement en binaire en utilisant un nombre fini de bits (tout comme 1/3 ne peut pas être représenté en décimal avec un nombre fini de chiffres). Comme le montre ce chapitre, les variables float et double en C sont stockées en binaire. Donc, les valeurs comme 23,85 ne peuvent pas être stockées exactement dans ce genre de variables. Seule une approximation de 23,85 peut être stockée.

Pour simplifier le matériel, les nombres en virgule flottante sont stockés dans un format standard. Ce format utilise la notation scientifique (mais en binaire, en utilisant des puissances de 2, pas de 10). Par exemple, 23,85 ou 10111, 11011001100110…2 serait stocké sous la forme :

1, 011111011001100110… × 2100

(où l'exposant (100) est en binaire). Un nombre en virgule flottante normalisé a la forme :

1, ssssssssssssssss × 2eeeeeee

où 1, sssssssssssss est la mantisse et eeeeeeee est l'exposant.

VII-A-2. Représentation en virgule flottante IEEE

L'IEEE (Institute of Electrical and Electronic Engineers) est une organisation internationale qui a conçu des formats binaires spécifiques pour stocker les nombres en virgule flottante. Ce format est utilisé sur la plupart des ordinateurs (mais pas tous !) fabriqués de nos jours. Souvent, il est supporté par le matériel de l'ordinateur lui-même. Par exemple, les coprocesseurs numériques (ou arithmétiques) d'Intel (qui sont intégrés à tous leurs processeurs depuis le Pentium) l'utilisent. L'IEEE définit deux formats différents avec des précisions différentes : simple et double précision. La simple précision est utilisée pour les variables float en C et la double précision pour les variables double.

Le coprocesseur arithmétique d'Intel utilise également une troisième précision plus élevée appelée précision étendue. En fait toutes les données dans le coprocesseur sont dans ce format. Lorsqu'elles sont stockées en mémoire depuis le coprocesseur, elles sont converties soit en simple, soit en double précision automatiquement (28). La précision étendue utilise un format général légèrement différent des formats float et double de l'IEEE, nous n'en parlerons donc pas ici.

VII-A-2-a. Simple précision IEEE

La virgule flottante simple précision utilise 32 bits pour encoder le nombre.

Elle est généralement précise jusqu'à 7 chiffres significatifs. Les nombres en virgule flottante sont généralement stockés dans un format beaucoup plus compliqué que celui des entiers.

La Figure 6.3 montre le format de base d'un nombre en simple précision IEEE.

Image non disponible

Ce format a plusieurs inconvénients. Les nombres en virgule flottante n'utilisent pas la représentation en complément à 2 pour les nombres négatifs. Ils utilisent une représentation en grandeur signée. Le bit 31 détermine le signe du nombre.

L'exposant binaire n'est pas stocké directement. À la place, la somme de l'exposant et de ProductID="7F" w:st="on">7F est stockée dans les bits 23 à 30, cet exposant décalé est toujours positif ou nul.

La partie décimale suppose une mantisse normalisée (de la forme 1, sssssssss).

Comme le premier bit est toujours un 1, le 1 de gauche n'est pas stocké ! Cela permet le stockage d'un bit additionnel à la fin et augmente donc légèrement la précision. Cette idée est appelée représentation en 1 masqué.

Il faut toujours garder à l'esprit que les octets 41 BE CC CD peuvent être interprétés de façons différentes selon ce qu'en fait le programme ! Vus comme un nombre en virgule flottante en simple précision, ils représentent 23,850000381, mais vus comme un entier double-mot, ils représentent 1,103,023,309 ! Le processeur ne sait pas quelle est la bonne interprétation !

Comment serait stocké 23,85 ? Tout d'abord, il est positif, donc le bit de signe est à 0. Ensuite, l'exposant réel est 4, donc l'exposant décalé est

ProductID="7F" w:st="on">7F + 4 = 8316.

Enfin, la fraction vaut 01111101100110011001100 (souvenez-vous que le 1 de tête est masqué). En les mettant bout à bout (pour clarifier les différentes sections du format en virgule flottante, le bit de signe et la fraction ont été soulignés et les bits ont été regroupés en groupes de 4 bits) :

Image non disponible

Cela ne fait pas exactement 23,85 (puisqu'il s'agit d'un binaire répétitif). Si l'on convertit le chiffre ci-dessus en décimal, on constate qu'il vaut approximativement 23,849998474, ce nombre est très proche de 23,85, mais n'est pas exactement le même. En réalité, en C, 23,85 ne serait pas représenté exactement comme ci-dessus. Comme le bit le plus à gauche qui a été supprimé de la représentation exacte valait 1, le dernier bit est arrondi à 1, donc 23,85 serait représenté par 41 BE CC CD en hexa en utilisant la simple précision. En décimal, ce chiffre vaut 23,850000381 ce qui est une meilleure approximation de 23,85.

Comment serait représenté -23,85 ? Il suffit de changer le bit de signe :

C1 BE CC CD. Ne prenez pas le complément à 2 !

Certaines combinaisons de e et f ont une signification spéciale pour les flottants IEEE. Le Tableau 6.1 décrit ces valeurs.

Image non disponible

Un infini est produit par un dépassement de capacité ou une division par zéro. Un résultat indéfini

est produit par une opération invalide comme rechercher la racine carrée d'un nombre négatif, additionner deux infinis, etc.

Les nombres en simple précision normalisés peuvent prendre des valeurs de

Image non disponible

VII-A-2-b. Nombres dénormalisés

Les nombres dénormalisés peuvent être utilisés pour représenter des nombres avec des grandeurs trop petites à normaliser (i.e. inférieures à 1, 0 × 2-126).

Par exemple, considérons le nombre Image non disponible

Dans la forme normalisée, l'exposant est trop petit. Par contre, il peut être représenté sous une forme non normalisée : 0, 010012 × 2-127. Pour stocker ce nombre, l'exposant décalé est positionné à 0 (voir Tableau 6.1) et la partie décimale est la mantisse complète du nombre écrite comme un produit avec 2-127 (i.e. tous les bits sont stockés, y compris le 1 à gauche du point décimal). La représentation de 1, 001 × 2-129 est donc :

Image non disponible

VII-A-2-c. Double précision IEEE

La double précision IEEE utilise 64 bits pour représenter les nombres et est précise jusqu'à environ 15 chiffres significatifs. Comme le montre la Figure 6.4, le format de base est très similaire à celui de la simple précision.

Image non disponible

Plus de bits sont utilisés pour l'exposant décalé (11) et la partie décimale (52).

La plage de valeurs plus grande pour l'exposant décalé a deux conséquences. La première est qu'il est calculé en faisant la somme de l'exposant réel et de 3FF (1023) (pas ProductID="7F" w:st="on">7F comme pour la simple précision). Deuxièmement, une grande plage d'exposants réels (et donc une plus grande plage de grandeurs) est disponible. Les grandeurs en double-précision peuvent prendre des valeurs entre environ 13-308 et 10308 .

C'est la plus grande taille du champ réservé à la partie décimale qui est reponsable de l'augmentation du nombre de chiffres significatifs pour les valeurs doubles.

Pour illustrer cela, reprenons 23,85. L'exposant décalé sera 4+3FF = 403 en hexa. Donc, la représentation double sera :

Image non disponible

ou 40 37 D9 99 99 99 99 9A en hexa. Si l'on reconvertit ce nombre en décimal, on trouve 23,8500000000000014 (il y a 12 zéros !) ce qui est une approximation de 23,85 nettement meilleure.

La double précision a les mêmes valeurs spéciales que la simple précision (29).

Les nombres dénormalisés sont également très similaires. La seule différence principale est que les nombres doubles dénormalisés utilisent 2-1023 au lieu de 2-127.

VII-B. Arithmétique en virgule flottante

L'arithmétique en virgule flottante sur un ordinateur est différente de celle des mathématiques. En mathématiques, tous les nombres peuvent être considérés comme exacts. Comme nous l'avons montré dans la section précédente, sur un ordinateur beaucoup de nombres ne peuvent pas être représentés exactement avec un nombre fini de bits. Tous les calculs sont effectués avec une précision limitée. Dans les exemples de cette section, des nombres avec une mantisse de 8 bits seront utilisés pour plus de simplicité.

VII-B-1. Addition

Pour additionner deux nombres en virgule flottante, les exposants doivent être égaux. S'ils ne le sont pas déjà, alors, il faut les rendre égaux en décalant la mantisse du nombre ayant le plus petit exposant. Par exemple, considérons

10, 375 + 6, 34375 = 16, 71875 ou en binaire :

Image non disponible

Ces deux nombres n'ont pas le même exposant, donc on décale la mantisse pour rendre les exposants égaux, puis on effectue l'addition :

Image non disponible

Notez que le décalage de 1, 1001011 × 22 supprime le 1 de tête et arrondi plus tard le résultat en 0, 1100110 × 23. Le résultat de l'addition, 10, 0001100 × 23 ou 1, 00001100 × 24), est égal à

10000,1102 ou 16,75, ce n'est pas égal à la réponse exacte (16,71875) ! Il ne s'agit que d'une approximation due aux erreurs d'arrondi du processus d'addition.

Il est important de réaliser que l'arithmétique en virgule flottante sur un ordinateur (ou une calculatrice) est toujours une approximation. Les lois des mathématiques ne fonctionnent pas toujours avec les nombres en virgule flottante sur un ordinateur. Les mathématiques supposent une précision infinie qu'aucun ordinateur ne peut atteindre. Par exemple, les mathématiques nous apprennent que (a + b) − b = a, cependant, ce n'est pas forcément exactement vrai sur un ordinateur !

VII-B-2. Soustraction

La soustraction fonctionne d'une façon similaire à l'addition et souffre des mêmes problèmes. Par exemple, considérons 16, 75 − 15, 9375 = 0, 8125 :

Image non disponible

Décaler 1, 1111111 × 23 donne (en arrondissant) 1, 0000000 × 24

Image non disponible

VII-B-3. Multiplication et division

Pour la multiplication, les mantisses sont multiplées et les exposants sont additionnés. Considérons 10, 375 × 2, 5 = 25, 9375 :

Image non disponible

Bien sûr, le résultat réel serait arrondi à 8 bits pour donner :

1, 1010000 × 24 = 11010, 0002 = 26

La division est plus compliquée, mais souffre des mêmes problèmes avec des erreurs d'arrondi.

VII-B-4. Conséquences sur la programmation

Le point essentiel de cette section est que les calculs en virgule flottante ne sont pas exacts. Le programmeur doit en être conscient. Une erreur courante que les programmeurs font avec les nombres en virgule flottante est de les comparer en supposant qu'un calcul est exact. Par exemple, considérons une fonction appelée f(x) qui effectue un calcul complexe et un programme qui essaie de trouver la racine de la fonction (30). On peut être tenté d'utiliser l'expression suivante pour vérifier si x est une racine :

Sélectionnez

if ( f(x) == 0.0 )

Mais, que se passe-t-il si f(x) retourne 1×10-30 ? Cela signifie très probablement que x est une très bonne approximation d'une racine réelle, cependant, l'égalité ne sera pas vérifiée. Il n'y a peut-être aucune valeur en virgule flottante IEEE de x qui renvoie exactement zéro, en raison des erreurs d'arrondi dans f(x).

Une meilleure méthode serait d'utiliser :

Sélectionnez

if ( fabs(f(x)) < EPS )

où EPS est une macro définissant une très petite valeur positive (du genre 1 x 10-10). Cette expression est vraie dès que f(x) est très proche de zéro.

En général, pour comparer une valeur en virgule flottante (disons x) à une autre (y), on utilise :

Sélectionnez

if ( fabs(x &#8722; y)/fabs(y) < EPS )

VII-C. Le coprocesseur arithmétique

VII-C-1. Matériel

Les premiers processeurs Intel n'avaient pas de support matériel pour les opérations en virgule flottante. Cela ne signifie pas qu'ils ne pouvaient pas effectuer de telles opérations. Cela signifie seulement qu'elles devaient être réalisées par des procédures composées de beaucoup d'instructions qui n'étaient pas en virgule flottante. Pour ces systèmes, Intel fournissait une puce appelée coprocesseur mathématique. Un coprocesseur mathématique a des instructions machine qui effectuent beaucoup d'opérations en virgule flottante beaucoup plus rapidement qu'en utilisant une procédure logicielle (sur les premiers processeurs, au moins 10 fois plus vite !). Le coprocesseur pour le 8086/8088 s'appelait le 8087, pour le 80286, il y avait un 80287 et pour le 80386, un 80387, le processeur 80486DX intégrait le coprocesseur mathématique dans le 80486 lui-même (31). Depuis le Pentium, toutes les générations de processeurs 80x86 ont un coprocesseur mathématique intégré. Cependant, on le programme toujours comme s'il s'agissait d'une unité séparée. Même les systèmes plus anciens sans coprocesseur peuvent installer un logiciel qui émule un coprocesseur mathématique. Cet émulateur est automatiquement activé lorsqu'un programme exécute une instruction du coprocesseur et lance la procédure logicielle qui produit le même résultat que celui qu'aurait donné le coprocesseur (bien que cela soit plus lent, bien sûr).

Le coprocesseur arithmétique a huit registres de virgule flottante. Chaque registre contient 80 bits de données. Les nombres en virgule flottante sont toujours stockés sous la forme de nombres 80 bits en précision étendue dans ces registres. Les registres sont appelés ST0, ST1, ST2… ST7. Les registres en virgule flottante sont utilisés différemment des registres entiers du processeur principal. Ils sont organisés comme une pile. Souvenez-vous qu'une pile est une liste Last-In First-Out (LIFO, dernier entré, premier sorti). ST0 fait toujours référence à la valeur au sommet de la pile. Tous les nouveaux nombres sont ajoutés au sommet de la pile. Les nombres existants sont décalés vers le bas pour faire de la place au nouveau.

Il y a également un registre de statut dans le coprocesseur arithmétique.

Il a plusieurs drapeaux. Seuls les 4 drapeaux utilisés pour les comparaisons seront traités : C0, C1, C2 et C3. Leur utilisation est traitée plus tard.

VII-C-2. Instructions

Pour faciliter la distinction entre les instructions du processeur normal et celles du coprocesseur, tous les mnémoniques du coprocesseur commencent par un F.

VII-C-2-a. Chargement et stockage

Il y a plusieurs instructions qui chargent des données au sommet de la pile du coprocesseur :

FLD source charge un nombre en virgule flottante depuis la mémoire vers

le sommet de la pile. La source peut être un nombre en simple,

double ou précision étendue ou un registre du coprocesseur.

FILD source lit un entier depuis la mémoire, le convertit en flottant et stocke le résultat au sommet de la pile. La source peut être un mot, un double-mot ou un quadruple-mot.

FLD1 Stocke un 1 au sommet de la pile.

FLDZ Stocke un zéro au sommet de la pile.

Il y a également plusieurs instructions qui déplacent les données depuis la pile vers la mémoire. Certaines de ces instructions retirent le nombre de la pile en même temps qu'elles le déplacent.

FST dest stocke le sommet de la pile (ST0) en mémoire. La destination peut être un nombre en simple ou double précision ou un registre du coprocesseur.

FSTP dest stocke le sommet de la pile en mémoire, exactement comme FST ; cependant, une fois le nombre stocké, sa valeur est retirée de la pile. La destination peut être un nombre en simple, double ou précision étendue ou un registre du coprocesseur.

FIST dest stocke la valeur au sommet de la pile convertie en entier en mémoire. La destination peut être un mot ou un double-mot. La pile en elle-même reste inchangée. La façon dont est converti le nombre en entier dépend de certains bits dans le mot de contrôle du coprocesseur. Il s'agit d'un registre d'un mot spécial (pas en virgule flottante) qui contrôle le fonctionnement du coprocesseur. Par défaut, le mot de contrôle est initialisé afin qu'il arrondisse à l'entier le plus proche lorsqu'il convertit vers un entier. Cependant, les instructions FSTCW (Store Control Word, stocker le mot de contrôle) et FLDCW (Load Control Word, charger le mot de contrôle) peuvent être utilisées pour changer ce comportement.

FISTP dest identique à FIST sauf en deux points. Le sommet de la pile est supprimé et la destination peut également être un quadruple-mot.

Il y a deux autres instructions qui peuvent placer ou retirer des données de la pile.

FXCH STn échange les valeurs de ST0 et STn sur la pile (où n est un numéro de registre entre 1 et 7).

FFREE STn libère un registre sur la pile en le marquant comme inutilisé ou vide.

VII-C-2-b. Addition et soustraction

Chacune des instructions d'addition calcule la somme de ST0 et d'un autre opérande. Le résultat est toujours stocké dans un registre du coprocesseur.

FADD source ST0 += source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FADD dest, ST0 dest += ST0. La destination peut être n'importe quel registre du coprocesseur.

FADDP dest ou FADDP dest, STO dest += ST0 puis ST0 est retiré de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIADD source ST0 += (float) source . Ajoute un entier à ST0. La source doit être un mot ou un double-mot en mémoire.

Il y a deux fois plus d'instructions pour la soustraction que pour l'addition, car l'ordre des opérandes est important pour la soustraction (i.e. a+b = b+a, mais a−b ≠ b−a !). Pour chaque instruction, il y a un miroir qui effectue la soustraction dans l'ordre inverse. Ces instructions inverses se finissent toutes soit par R soit par RP. La Figure 6.5 montre un court extrait de code qui ajoute les éléments d'un tableau de doubles.

Image non disponible

Au niveau des lignes 10 et 13, il faut spécifier la taille de l'opérande mémoire. Sinon l'assembleur ne saurait pas si l'opérande mémoire est un float (dword) ou un double (qword).

FSUB source ST0 -= source. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FSUBR source ST0 = source - ST0. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FSUB dest, ST0 dest -= ST0. La destination peut être n'importe quel registre du coprocesseur.

FSUBR dest, ST0 dest = ST0 - dest . La destination peut être n'importe quel registre du coprocesseur.

FSUBP dest ou FSUBP dest, STO dest -= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FSUBRP dest ou FSUBRP dest, ST0 dest = ST0 - dest puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FISUB source ST0 -= (float) source . Soustrait un entier de ST0. La source doit être un mot ou un double mot en mémoire.

FISUBR source ST0 = (float) source - ST0. Soustrait ST0 d'un entier. La source doit être un mot ou un double-mot en mémoire.

VII-C-2-c. Multiplication et division

Les instructions de multiplication sont totalement analogues à celles d'addition.

FMUL source ST0 *= source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FMUL dest, ST0 dest *= ST0. La destination peut être n'importe quel registre du coprocesseur.

FMULP dest ou FMULP dest, ST0 dest *= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIMUL source ST0 *= (float) source. Multiplie un entier avec ST0. La source peut être un mot ou un double-mot en mémoire.

Ce n'est pas étonnant, les instructions de division sont analogues à celles de soustraction. La division par zéro donne l'infini.

FDIV source ST0 /= source . La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FDIVR source ST0 = source / ST0. La source peut être n'importe quel registre du coprocesseur ou un nombre en simple ou double précision en mémoire.

FDIV dest, ST0 dest /= ST0. La destination peut être n'importe quel registre du coprocesseur.

FDIVR dest, ST0 dest = ST0 / dest . La destination peut être n'importe quel registre du coprocesseur.

FDIVP dest ou FDIVP dest, ST0 dest /= ST0 puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FDIVRP dest ou FDIVRP dest, ST0 dest = ST0 / dest puis retire ST0 de la pile. La destination peut être n'importe quel registre du coprocesseur.

FIDIV source ST0 /= (float) source. Divise ST0 par un entier. La src doit être un mot ou un double-mot en mémoire.

FIDIVR source ST0 = (float) source / ST0. Divise un entier par ST0. La source doit être un mot ou un double-mot en mémoire.

VII-C-2-d. Comparaisons

Le coprocesseur effectue également des comparaisons de nombres en virgule flottante. La famille d'instructions FCOM est faite pour ça.

FCOM source compare ST0 et source. La source peut être un registre du coprocesseur ou un float ou un double en mémoire.

FCOMP source compare ST0 et source, puis retire ST0 de la pile. La source peut être un registre du coprocesseur ou un float ou un double en mémoire.

FCOMPP compare ST0 et ST1, puis retire ST0 et ST1 de la pile.

FICOM source compare ST0 et (float) source. La source peut être un entier sur un mot ou un double-mot en mémoire.

FICOMP source compare ST0 et (float)source, puis retire ST0 de la pile. La source peut être un entier sur un mot ou un double-mot en mémoire.

FTST compare ST0 et 0,

Ces instructions changent les bits C0, C1, C2 et C3 du registre de statut du coprocesseur. Malheureusement, il n'est pas possible pour le processeur d'accéder à ces bits directement. Les instructions de branchement conditionnel utilisent le registre FLAGS, pas le registre de statut du coprocesseur.

Cependant, il est relativement simple de transférer les bits du mot de statut dans les bits correspondants du registre FLAGS en utilisant quelques instructions nouvelles :

FSTSW destination stocke le mot de statut du coprocesseur soit dans un mot en mémoire soit dans le registre AX.

SAHF stocke le registre AH dans le registre FLAGS.

LAHF charge le registre AH avec les bits du registre FLAGS.

La Figure 6.6 montre un court extrait de code en exemple.

Image non disponible

Les lignes 5 et 6 transfèrent les bits C0, C1, C2 et C3 du mot de statut du coprocesseur dans le registre FLAGS. Les bits sont transférés de façon à être identiques au résultat d'une comparaison de deux entiers non signés. C'est pourquoi la ligne 7 utilise une instruction JNA.

Les Pentium Pro (et les processeurs plus récents (Pentium II and III)) supportent deux nouveaux opérateurs de comparaison qui modifient directement le registre FLAGS du processeur.

FCOMI source compare ST0 et source. La source doit être un registre du coprocesseur.

FCOMIP source compare ST0 et source, puis retire ST0 de la pile. La source doit être un registre du coprocesseur.

La Figure 6.7 montre une sous-routine qui trouve le plus grand de deux double en utilisant l'instruction FCOMIP. Ne confondez pas ces instructions avec les fonctions de comparaisons avec un entier (FICOM et FICOMP).

Image non disponible

VII-C-2-e. Instructions diverses

Cette section traite de diverses autres instructions fournies par le coprocesseur.

FCHS ST0 = - ST0 change le signe de ST0.

FABS ST0 = |ST0| extrait la valeur absolue de ST0.

FSQRT extrait la racine carrée de ST0.

FSCALE ST0 = ST0 × 2[ST1] multiplie ST0 par une puissance de 2 rapidement. ST1 n'est pas retiré de la pile du coprocesseur.

La Figure 6.8 montre un exemple d'utilisation de cette instruction.

Image non disponible

VII-C-3. Exemple : formule quadratique

Le premier exemple montre comment la formule quadratique peut être codée en assembleur. Souvenez-vous, la formule quadratique calcule les solutions de l'équation :

ax2 + bx + c = 0

La formule donne deux solutions pour x : x1 et x2.

Image non disponible

L'expression sous la racine carrée (b2 − 4ac) est appelée le déterminant. Sa valeur est utile pour déterminer laquelle des trois possibilités suivantes est vérifiée pour les solutions :

1. Il n'y a qu'une seule solution double. b2 − 4ac = 0 ;

2. Il y a deux solutions réelles. b2 − 4ac > 0 ;

3. Il y a deux solutions complexes. b2 − 4ac < 0.

Voici un court programme C qui utilise la sous-routine assembleur :

quadt.c

Sélectionnez

2
3 int quadratic ( double, double, double, double , double );
4
5  int main()
6 {
7 double a,b,c , root1 , root2;
8
9 printf ("Entrez a , b, c : ");
10 scanf("%lf %lf %lf", &a, &b, &c);
11 if ( quadratic ( a , b, c, &root1, &root2 ) )
12 printf (" racines : %.10g %.10g\n", root1, root2 );
13 else
14 printf ("pas de racine réelle \n");
15 return 0;
16 }

Voici la routine assembleur :

quad.asm

Sélectionnez

1 ; fonction quadratic
2 ; trouve les solutions à l'équation quadratique :
3 ; a*x**2 + b*x + c = 0
4 ; prototype C :
5 ; int quadratic( double a, double b, double c,
6 ; double * root1, double *root2 )
7 ; Paramètres :
8 ; a, b, c - coefficients des puissances de l'équation quadratique (voir ci-dessus)
9 ; root1 - pointeur vers un double où stocker la première racine
10 ; root2 - pointeur vers un double où stocker la deuxième racine
11 ; Valeur de retour :
12 ; retourne 1 si des racines réelles sont trouvées, sinon 0
13
14 ?fine a qword [ebp+8]
15 ?fine b qword [ebp+16]
16 ?fine c qword [ebp+24]
17 ?fine root1 dword [ebp+32]
18 ?fine root2 dword [ebp+36]
19 ?fine disc qword [ebp-8]
20 ?fine one_over_2a qword [ebp-16]
21
22 segment .data
23 MinusFour dw -4
24
25 segment .text
26 global _quadratic
27 _quadratic:
28 push ebp
29 mov ebp, esp
30 sub esp, 16 ; alloue 2 doubles (disc & one_over_2a)
31 push ebx ; on doit sauvegarder l'ebx original
32
33 fild word [MinusFour]; pile : -4
34 fld a ; pile : a, -4
35 fld c ; pile : c, a, -4
36 fmulp st1 ; pile : a*c, -4
37 fmulp st1 ; pile : -4*a*c
38 fld b
39 fld b ; pile : b, b, -4*a*c
40 fmulp st1 ; pile : b*b, -4*a*c
41 faddp st1 ; pile : b*b - 4*a*c
42 ftst ; compare avec 0
43 fstsw ax
44 sahf
45 jb no_real_solutions ; si < 0, pas de solution réelle
46 fsqrt ; pile : sqrt(b*b - 4*a*c)
47 fstp disc ; stocke et décale la pile
48 fld1 ; pile : 1,0
49 fld a ; pile : a, 1,0
50 fscale ; pile : a * 2(1,0) = 2*a, 1
51 fdivp st1 ; pile : 1/(2*a)
52 fst one_over_2a ; pile : 1/(2*a)
53 fld b ; pile : b, 1/(2*a)
54 fld disc ; pile : disc, b, 1/(2*a)
55 fsubrp st1 ; pile : disc - b, 1/(2*a)
56 fmulp st1 ; pile : (-b + disc)/(2*a)
57 mov ebx, root1
58 fstp qword [ebx] ; stocke dans *root1
59 fld b ; pile : b
60 fld disc ; pile : disc, b
61 fchs ; pile : -disc, b
62 fsubrp st1 ; pile : -disc - b
63 fmul one_over_2a ; pile : (-b - disc)/(2*a)
64 mov ebx, root2
65 fstp qword [ebx] ; stocke dans *root2
66 mov eax, 1 ; la valeur de retour est 1
67 jmp short quit
68
69 no_real_solutions :
70 mov eax, 0 ; la valeur de retour est 0
71
72 quit :
73 pop ebx
74 mov esp, ebp
75 pop ebp
76 ret

VII-C-4. Exemple : lire un tableau depuis un fichier

Dans cet exemple, une routine assembleur lit des doubles depuis un fichier. Voici un court programme de test en C :

readt.c

Sélectionnez

1 /
2 Ce programme teste la procédure assembleur 32 bits read_doubles().
3 Il lit des doubles depuis stdin (utilisez une redirection pour lire depuis un fichier).
4 /
5 #include <stdio.h>
6 extern int read_doubles( FILE , double , int );
7 #dene MAX 100
8
9 int main()
10 {
11 int i ,n;
12 double a[MAX];
13
14 n = read_doubles(stdin, a , MAX);
15
16 for ( i=0; i < n; i++ )
17 printf ("= %g\n", i, a[i ]);
18 return 0;
19 }

Voici la routine assembleur :

read.asm

Sélectionnez

1 segment .data
2 format db "%lf", 0 ; format pour fscanf()
3,
4 segment .text
5 global _read_doubles
6 extern _fscanf
7
8 ?fine SIZEOF_DOUBLE 8
9 ?fine FP dword [ebp + 8]
10 ?fine ARRAYP dword [ebp + 12]
11 ?fine ARRAY_SIZE dword [ebp + 16]
12 ?fine TEMP_DOUBLE [ebp - 8]
13
14 ;
15 ; fonction _read_doubles
16 ; prototype C :
17 ; int read_doubles( FILE * fp, double * arrayp, int array_size );
18 ; Cette fonction lit des doubles depuis un fichier texte dans un tableau, jusqu'à
19 ; EOF ou que le tableau soit plein.
20 ; Paramètres :
21 ; fp - FILE pointeur à partir duquel lire (doit être ouvert en lecture)
22 ; arrayp - pointeur vers le tableau de double vers lequel lire
23 ; array_size - nombre d'éléments du tableau
24 ; Valeur de retour :
25 ; nombre de doubles stockés dans le tableau (dans EAX)
26
27 _read_doubles:
28 push ebp
29 mov ebp,esp
30 sub esp, SIZEOF_DOUBLE ; définit un double sur la pile
31
32 push esi ; sauve esi
33 mov esi, ARRAYP ; esi = ARRAYP
34 xor edx, edx ; edx = indice du tableau (initialement 0)
35
36 while_loop:
37 cmp edx, ARRAY_SIZE ; edx < ARRAY_SIZE ?
38 jnl short quit ; si non, quitte la boucle
39 ;
40 ; appelle fscanf() pour lire un double dans TEMP_DOUBLE
41 ; fscanf() peut changer edx, donc on le sauvegarde
42 ;
43 push edx ; sauve edx
44 lea eax, TEMP_DOUBLE
45 push eax ; empile &TEMP_DOUBLE
46 push dword format ; emplie &format
47 push FP ; emplie file pointer
48 call _fscanf
49 add esp, 12
50 pop edx ; restaure edx
51 cmp eax, 1 ; fscanf a retourné 1 ?
52 jne short quit ; si non, on quitte la boucle
53
54 ;
55 ; copie TEMP_DOUBLE dans ARRAYP[edx]
56 ; (Les 8 octets du double sont copiés en deux fois 4 octets)
57 ;
58 mov eax, [ebp - 8]
59 mov [esi + 8*edx], eax ; copie des 4 octets de poids faible
60 mov eax, [ebp - 4]
61 mov [esi + 8*edx + 4], eax ; copie des 4 octets de poids fort
62
63 inc edx
64 jmp while_loop
65
66 quit :
67 pop esi ; restaure esi
68
69 mov eax, edx ; stocke la valeur de retour dans eax
70
71 mov esp, ebp
72 pop ebp
73 ret

VII-C-5. Exemple : rechercher les nombres premiers

Ce dernier exemple recherche les nombres premiers, une fois de plus.

Cette implémentation est plus efficace que la précédente. Elle stocke les nombres premiers déjà trouvés dans un tableau et ne divise que par ceux-ci au lieu de diviser par tous les nombres impairs pour trouver de nouveaux premiers.

Une autre différence est qu'il calcule la racine carrée du candidat pour le prochain premier afin de déterminer le moment où il peut s'arrêter de rechercher des facteurs. Il altère le fonctionnement du coprocesseur afin que lorsqu'il stocke la racine sous forme d'entier, il la tronque au lieu de l'arrondir.

Ce sont les bits 10 et 11 du mot de contrôle qui permettent de paramétrer cela. Ces bits sont appelés les bits RC (Rounding Control, contrôle d'arrondi).

S'ils sont tous les deux à 0 (par défaut), le coprocesseur arrondit lorsqu'il convertit vers un entier. S'ils sont tous les deux à 1, le copresseur tronque les conversions en entiers. Notez que la routine fait attention à sauvegarder le mot de contrôle original et à le restaurer avant de quitter.

Voici le programme C pilote :

fprime.c

Sélectionnez

1 #include <stdio.h>
2 #include <stdlib.h>
3 /
4 fonction nd_primes
5 recherche le nombre indiqué de nombres premiers
6 èParamtres :
ProductID="7 a" w:st="on">7 a &#8722; tableau pour contenir les nombres premiers
8 n &#8722; nombre de nombres premiers à trouver
9 /
10 extern void nd_primes( int a , unsigned n );
11
12 int main()
13 {
14 int statut ;
15 unsigned i;
16 unsigned max;
17 int a;
18
19 printf ("Combien de nombres premiers voulez-vous trouver ? ");
20 scanf("%u", &max);
21
ProductID="22 a" w:st="on">22 a = calloc ( sizeof(int ), max);
23
24 if ( a ) {
25
26 nd_primes(a,max);
27
28 / affiche les 20 derniers nombres premiers trouvés /
29 for( i= ( max > 20 ) ? max &#8722; 20 : 0; i < max; i++ )
30 printf ("= %d\n", i+1, a[i]);
31
32 free (a);
33 statut = 0;
34 }
35 else {
36 fprintf ( stderr , "Impossible de  créer un tableau de %u entiers\n", max);
37 statut = 1;
38 }
39
40 return statut ;
41 }

Voici la routine assembleur :

prime2.asm

Sélectionnez

1 segment .text
2 global _find_primes
3 ;
4 ; fonction find_primes
5 ; trouve le nombre indiqué de nombres premiers
6 ; Paramètres :
7 ; array - tableau pour contenir les nombres premiers
8 ; n_find - nombre de nombres premiers à trouver
9 ; Prototype C :
10 ;extern void find_primes( int * array, unsigned n_find )
11 ;
12 ?fine array ebp + 8
13 ?fine n_find ebp + 12
14 ?fine n ebp - 4 ; Nombre de nombres premiers trouvés
15 ?fine isqrt ebp - 8 ; racine du candidat
16 ?fine orig_cntl_wd ebp - 10 ; mot de contrôle original
17 ?fine new_cntl_wd ebp - 12 ; nouveau mot de contrôle
18
19 _find_primes :
20 enter 12,0 ; fait de la place pour les variables locales
21
22 push ebx ; sauvegarde les variables registre éventuelles
23 push esi
24
25 fstcw word [orig_cntl_wd] ; récupère le mot de contrôle courant
26 mov ax, [orig_cntl_wd]
27 or ax, 0C00h ; positionne les bits d'arrondi à 11 (tronquer)
28 mov [new_cntl_wd], ax
29 fldcw word [new_cntl_wd]
30
31 mov esi, [array] ; esi pointe sur array
32 mov dword [esi], 2 ; array[0] = 2
33 mov dword [esi + 4], 3 ; array[1] = 3
34 mov ebx, 5 ; ebx = guess = 5
35 mov dword [n], 2 ; n = 2
36 ;
37 ; Cette boucle externe trouve un nouveau nombre premier à chaque itération qu'il
38 ; ajoute à la fin du tableau. Contrairement au programme de recherche de nombres
39 ; premier précédent, cette fonction ne détermine pas la primauté du nombre en
40 ; divisant par tous les nombres impairs. Elle ne divise que par les nombres
41 ; premiers qu'elle a déjà trouvé (c'est pourquoi ils sont stockés dans un tableau).
42 ;
43 while_limit:
44 mov eax, [n]
45 cmp eax, [n_find] ; while ( n < n_find )
46 jnb short quit_limit
47
48 mov ecx, 1 ; ecx est utilisé comme indice
49 push ebx ; stocke le candidat sur la pile
50 fild dword [esp] ; le charge sur la pile du coprocesseur
51 pop ebx ; retire le candidat de la pile
52 fsqrt ; calcule sqrt(guess)
53 fistp dword [isqrt] ; isqrt = floor(sqrt(quess))
54 ;
55 ; Cette boucle interne divise le candidat (ebx) par les nombres premiers
56 ; calculés précédemment jusqu'à ce qu'il trouve un de ses facteurs premiers
57 ; (ce qui signifie que le candidat n'est pas premier) ou jusqu'à ce que le
58 ; nombre premier par lequel diviser soit plus grand que floor(sqrt(guess))
59 ;
60 while_factor:
61 mov eax, dword [esi + 4*ecx] ; eax = array[ecx]
62 cmp eax, [isqrt] ; while ( isqrt < array[ecx]
63 jnbe short quit_factor_prime
64 mov eax, ebx
65 xor edx, edx
66 div dword [esi + 4*ecx]
67 or edx, edx ; && guess % array[ecx] != 0 )
68 jz short quit_factor_not_prime
69 inc ecx ; essaie le nombre premier suivant
70 jmp short while_factor
71
72 ;
73 ; found a new prime !
74 ;
75 quit_factor_prime :
76 mov eax, [n]
77 mov dword [esi + 4*eax], ebx ; ajoute le candidat au tableau
78 inc eax
79 mov [n], eax ; incrémente n
80
81 quit_factor_not_prime :
82 add ebx, 2 ; essaie le nombre impair suivant
83 jmp short while_limit
84
85 quit_limit:
86
87 fldcw word [orig_cntl_wd] ; restaure le mot de contrôle
88 pop esi ; restaure les variables registre
89 pop ebx
90
91 leave
92 ret

VIII. Structures et C++

VIII-A. Structures

VIII-A-1. Introduction

Les structures sont utilisées en C pour regrouper des données ayant un rapport entre elles dans une variable composite. Cette technique a plusieurs avantages :

1. Cela clarifie le code en montrant que les données définies dans la structure sont intimement liées ;

2. Cela simplifie le passage des données aux fonctions. Au lieu de passer plusieurs variables séparément, elles peuvent être passées en une seule entité ;

3. Cela augmente la localité (32) du code.

Du point de vue de l'assembleur, une structure peut être considérée comme un tableau avec des éléments de tailles variables. Les éléments des vrais tableaux sont toujours de la même taille et du même type. C'est cette propriété qui permet de calculer l'adresse de n'importe quel élément en connaissant l'adresse de début du tableau, la taille des éléments et l'indice de l'élément voulu.

Les éléments d'une structure ne sont pas nécessairement de la même taille (et habituellement, ils ne le sont pas). À cause de cela, chaque élément d'une structure doit être explicitement spécifié et doit recevoir un tag (ou nom) au lieu d'un indice numérique.

En assembleur, on accède à un élément d'une structure d'une façon similaire à l'accès à un élément de tableau. Pour accéder à un élément, il faut connaître l'adresse de départ de la structure et le déplacement relatif de cet un tableau où ce déplacement peut être calculé grâce à l'indice de l'élément, c'est le compilateur qui affecte un déplacement aux éléments d'une structure.

Par exemple, considérons la structure suivante :

Sélectionnez

struct S {

short int x ; / entier sur 2 octets /

int y ; / entier sur 4 octets /

double z ; / ottant sur 8 octets /

};

La Figure 7.1 montre à quoi pourrait ressembler une variable de type S en mémoire.

Image non disponible

Le standard ANSI C indique que les éléments d'une structure sont organisés en mémoire dans le même ordre que celui de leur définition dans le struct. Il indique également que le premier élément est au tout début de la structure (i.e. au déplacement zéro). Il définit également une macro utile dans le fichier d'en-tête stddef.h appelée offsetof(). Cette macro calcule et renvoie le déplacement de n'importe quel élément d'une structure. La macro prend deux paramètres, le premier est le nom du type de la structure, le second est le nom de l'élément dont on veut le déplacement. Donc le résultat de offsetof(S, y) serait 2 d'après la Figure 7.1.

VIII-A-2. Alignement en mémoire

Si l'on utilise la macro offsetof pour trouver le déplacement de y en utilisant le compilateur gcc, on s'aperçoit qu'elle renvoie 4, pas 2 ! Pourquoi ?

Parce que gcc (et beaucoup d'autres compilateurs) aligne les variables sur des multiples de double-mot par défaut.

Souvenez-vous qu'une adresse est sur un multiple de double-mot si elle est divisible par 4.

En mode protégé 32 bits, le processeur lit la mémoire plus vite si la donnée commence sur un multiple de double-mot. La Figure 7.2 montre à quoi ressemble la structure S en utilisant gcc.

Image non disponible

Le compilateur insère deux octets inutilisés dans la structure pour aligner y (et z) sur un multiple de double-mot. Cela montre pourquoi c'est une bonne idée d'utiliser offsetof pour calculer les déplacements, au lieu de les calculer soi-même lorsqu'on utilise des structures en C.

Bien sûr, si la structure est utilisée uniquement en assembleur, le programmeur peut déterminer les déplacements lui-même. Cependant, si l'on interface du C et de l'assembleur, il est très important que l'assembleur et le C s'accordent sur les déplacements des éléments de la structure ! Une des complications est que des compilateurs C différents peuvent donner des déplacements différents aux éléments. Par exemple, comme nous l'avons vu, le compilateur gcc crée une structure S qui ressemble à la Figure 7.2, cependant, le compilateur de Borland créerait une structure qui ressemble à la Figure 7.1. Les compilateurs C fournissent le moyen de spécifier l'alignement utilisé pour les données. Cependant, le standard ANSI C ne spécifie pas comment cela doit être fait et donc, des compilateurs différents procèdent différemment.

Le compilateur gcc a une méthode flexible et compliquée de spécifier l'alignement. Le compilateur permet de spécifier l'alignement de n'importe quel type en utilisant une syntaxe spéciale. Par exemple, la ligne suivante :

Sélectionnez

typedef short int unaligned_int __attribute__((aligned(1)));

définit un nouveau type appelé unaligned_int qui est aligné sur des multiples d'octet (oui, toutes les parenthèses suivant __attribute__ sont nécessaires !) Le paramètre 1 de aligned peut être remplacé par d'autres puissances de 2 pour spécifier d'autres alignements (2 pour s'aligner sur les mots, 4 sur les doubles-mots, etc.). Si l'élément y de la structure était changé en un type unaligned_int, gcc placerait y au déplacement 2. Cependant, z serait toujours au déplacement 8 puisque les doubles sont également alignés sur des doubles-mots par défaut. La définition du type de z devrait aussi être changée pour le placer au déplacement 6.

Le compilateur gcc permet également de comprimer (pack) une structure.

Cela indique au compilateur d'utiliser le minimum d'espace possible pour la structure. La Figure 7.3 montre comment S pourrait être réécrite de cette façon. Cette forme de S utiliserait le moins d'octets possible, soit 14 octets.

Image non disponible

Les compilateurs de Microsoft et Borland supportent tous les deux la même méthode pour indiquer l'alignement par le biais d'une directive #pragma.

Sélectionnez

#pragma pack(1)

La directive ci-dessus indique au compilateur d'aligner les éléments des structures sur des multiples d'un octet (i.e., sans décalage superflu). Le 1 peut être remplacé par 2, 4, 8 ou 16 pour spécifier un alignement sur des multiples de mots, doubles-mots, quadruples-mots ou de paragraphe, respectivement. La directive reste active jusqu'à ce qu'elle soit écrasée par une autre. Cela peut poser des problèmes puisque ces directives sont souvent utilisées dans des fichiers d'en-tête. Si le fichier d'en-tête est inclus avant d'autres fichiers d'en-tête définissant des structures, ces structures peuvent être organisées différemment de ce qu'elles auraient été par défaut. Cela peut conduire à des erreurs très difficiles à localiser. Les différents modules d'un programme devraient organiser les éléments des structures à différents endroits !

Il y a une façon d'éviter ce problème. Microsoft et Borland permettent la sauvegarde de l'état de l'alignement courant et sa restauration. La Figure 7.4 montre comment on l'utilise.

Image non disponible

VIII-A-3. Champs de bits

Les champs de bits permettent de déclarer des membres d'une structure qui n'utilisent qu'un nombre de bits donné. La taille en bits n'a pas besoin d'être un multiple de 8. Un membre champ de bit est défini comme un unsigned int ou un int suivi de deux-points et de sa taille en bits. La Figure 7.5 en montre un exemple.

Image non disponible

Elle définit une variable 32 bits décomposée comme suit :

Image non disponible

Le premier champ de bits est assigné aux bits les moins significatifs du double-mot (33).

Néanmoins, le format n'est pas si simple si l'on observe comment les bits sont stockés en mémoire. La difficulté apparaît lorsque les champs de bits sont à cheval sur des multiples d'octets. Car les octets, sur un processeur little endian seront inversés en mémoire. Par exemple, les champs de bits de la structure S ressembleront à cela en mémoire :

Image non disponible

L'étiquette f2l fait référence aux cinq derniers bits (i.e., les cinq bits les moins significatifs) du champ de bits f2. L'étiquette f2m fait référence aux cinq bits les plus significatifs de f2. Les lignes verticales doubles montrent les limites d'octets. Si l'on inverse tous les octets, les morceaux des champs f2 et f3 seront réunis correctement.

L'organisation de la mémoire physique n'est habituellement pas importante à moins que des données soient transférées depuis ou vers le programme (ce qui est en fait assez courant avec les champs de bits). Il est courant que les interfaces de périphériques matériels utilisent des nombres impairs de bits dont les champs de bits facilitent la représentation.

Un bon exemple est SCSI (34). Une commande de lecture directe pour un périphérique SCSI est spécifiée en envoyant un message de six octets au périphérique selon le format indiqué dans la Figure 7.6.

Image non disponible

La difficulté de représentation en utilisant les champs de bits est l'adresse de bloc logique qui est à cheval sur trois octets différents de la commande. D'après la Figure 7.6, on constate que les données sont stockées au format big endian. La Figure 7.7 montre une définition qui essaie de fonctionner avec tous les compilateurs.

Image non disponible

Les deux premières lignes définissent une macro qui est vraie si le code est compilé avec un compilateur Borland ou Microsoft. La partie qui peut porter à confusion va des lignes 11 à 14. Tout d'abord, on peut se demander pourquoi les champs lba_mid et lba_lsb sont définis séparément et non pas comme un champ unique de 16 bits. C'est parce que les données sont stockées au format big endian. Un champ de 16 bits serait stocké au format little endian par le compilateur. Ensuite, les champs lba_msb et logical_unit semblent être inversés, cependant, ce n'est pas le cas. Ils doivent être placés dans cet ordre. La Figure 7.8 montre comment les champs sont organisés sous la forme d'une entité de 48 bits (les limites d'octets sont là encore représentées par des

lignes doubles).

Image non disponible

Lorsqu'elle est stockée en mémoire au format little endian, les bits sont réarrangés au format voulu (Figure 7.6).

Pour compliquer encore plus le problème, la définition de SCSI_read_cmd ne fonctionne pas correctement avec le C Microsoft. Si l'expression sizeof(SCSI_read_cmd) est évaluée, le C Microsoft renvoie 8 et non pas 6 ! C'est parce que le compilateur Microsoft utilise le type du champ de bits pour déterminer comment organiser les bits. Comme tous les bits sont déclarés comme unsigned, le compilateur ajoute deux octets à la fin de la structure pour qu'elle comporte un nombre entier de doubles-mots. Il est possible d'y remédier en déclarant tous les champs unsigned short. Maintenant, le compilateur Microsoft n'a plus besoin d'ajouter d'octets d'alignement puisque six octets forment un nombre entier de mots de deux octets (35). Les autres compilateurs fonctionnent également correctement avec ce changement. La Figure 7.9 montre une autre définition qui fonctionne sur les trois compilateurs.

Image non disponible

Elle ne déclare plus que deux champs de bits en utilisant le type unsigned char.

Le lecteur ne doit pas se décourager s'il trouve la discussion ci-dessus confuse. C'est confus ! L'auteur trouve souvent moins confus d'éviter d'utiliser des champs de bits en utilisant des opérations niveau bit pour examiner et modifier les bits manuellement.

VIII-A-4. Utiliser des structures en assembleur

Comme nous l'avons dit plus haut, accéder à une structure en assembleur ressemble beaucoup à accéder à un tableau. Prenons un exemple simple, regardons comment l'on pourrait écrire une routine assembleur qui mettrait à zéro l'élément y d'une structure S. Supposons que le prototype de la routine soit :

Sélectionnez

void zero_y( S s_p );

La routine assembleur serait :

Sélectionnez

1 ?fine y_offset 4
2 _zero_y :
3 enter 0,0
4 mov eax, [ebp + 8] ; récupère s_p depuis la pile
5 mov dword [eax + y_offset], 0
6 leave
7 ret

Le C permet de passer une structure par valeur à une fonction. Cependant, c'est une mauvaise idée la plupart du temps. Toutes les données de la structure doivent être copiées sur la pile puis récupérées par la routine. Il est beaucoup plus efficace de passer un pointeur vers la structure à la place.

Le C permet aussi qu'une fonction renvoie une structure. Évidemment, une structure ne peut pas être retournée dans le registre EAX. Des compilateurs différents gèrent cette situation de façon différente. Une situation courante que les compilateurs utilisent est de réécrire la fonction en interne de façon à ce qu'elle prenne un pointeur sur la structure en paramètre. Le pointeur est utilisé pour placer la valeur de retour dans une structure définie en dehors de la routine appelée.

La plupart des assembleurs (y compris NASM) ont un support intégré pour définir des structures dans votre code assembleur. Reportez-vous à votre documentation pour plus de détails.

VIII-B. Assembleur et C++

Le langage de programmation C++ est une extension du langage C. Beaucoup des règles valables pour interfacer le C et l'assembleur s'appliquent également au C++. Cependant, certaines règles doivent être modifiées. De plus, certaines extensions du C++ sont plus faciles à comprendre en connaissant le langage assembleur. Cette section suppose une connaissance basique du C++.

VIII-B-1. Surcharge et décoration de noms

Le C++ permet de définir des fonctions (et des fonctions membres) différentes avec le même nom. Lorsque plus d'une fonction partage le même nom, les fonctions sont dites surchargées. Si deux fonctions sont définies avec le même nom en C, l'éditeur de liens produira une erreur, car il trouvera deux définitions pour le même symbole dans les fichiers objet qu'il est en train de lier. Par exemple, prenons le code de la Figure 7.10.

Image non disponible

Le code assembleur équivalent définirait deux étiquettes appelées _f ce qui serait bien sûr une

erreur. Le C++ utilise le même procédé d'édition de liens que le C, mais évite cette erreur en effectuant une décoration de nom (name mangling) ou en modifiant le symbole utilisé pour nommer une fonction. D'une certaine façon, le C utilise déjà la décoration de nom. Il ajoute un caractère de soulignement au nom de la fonction C lorsqu'il crée l'étiquette pour la fonction. Cependant, il décorera le nom des deux fonctions de la Figure 7.10 de la même façon et produira une erreur. Le C++ utilise un procédé de décoration plus sophistiqué qui produit deux étiquettes différentes pour les fonctions. Par exemple, la première fonction de la Figure 7.10 recevrait l'étiquette _f__Fi et la seconde, _f__Fd, sous DJGPP. Cela évite toute erreur d'édition de liens.

Malheureusement, il n'y a pas de standard sur la gestion des noms en C++ et des compilateurs différents décorent les noms de façon différente.

Par exemple, Borland C++ utiliserait les étiquettes @f$qi et @f$qd pour les deux fonctions de la Figure 7.10. Cependant, les règles ne sont pas totalement arbitraires. Le nom décoré encode la signature de la fonction. La signature d'une fonction est donnée par l'ordre et le type de ses paramètres. Notez que la fonction qui ne prend qu'un argument int a un i à la fin de son nom décoré (à la fois sous DJGPP et Borland) et que celle qui prend un argument double a un d à la fin de son nom décoré. S'il y avait une fonction appelée f avec le prototype suivant :

Sélectionnez

void f ( int x , int y , double z);

DJGPP décorerait son nom en _f__Fiid et Borland en @f$qiid.

Le type de la fonction ne fait pas partie de la signature d'une fonction et n'est pas encodé dans le nom décoré. Ce fait explique une règle de la surcharge en C++. Seules les fonctions dont les signatures sont uniques peuvent être surchargées. Comme on le voit, si deux fonctions avec le même nom et la même signature sont définies en C++, elles donneront le même nom décoré et créeront une erreur lors de l'édition de liens. Par défaut, toutes les fonctions C++ sont décorées, même celles qui ne sont pas surchargées. Lorsqu'il compile un fichier, le compilateur n'a aucun moyen de savoir si une fonction particulière est surchargée ou non, il décore donc tous les noms. En fait, il décore également les noms des variables globales en encodant le type de la variable d'une façon similaire à celle utilisée pour les signatures de fonctions.

Donc, si l'on définit une variable globale dans un fichier avec un certain type puis que l'on essaie de l'utiliser dans un autre fichier avec le mauvais type, l'éditeur de liens produira une erreur. Cette caractéristique du C++ est connue sous le nom de typesafe linking (édition de liens avec respect des types) . Cela crée un autre type d'erreurs, les prototypes inconsistants. Cela arrive lorsque la définition d'une fonction dans un module ne correspond pas avec le prototype utilisé par un autre module. En C, cela peut être un problème très difficile à corriger. Le C ne détecte pas cette erreur. Le programme compilera et sera lié, mais aura un comportement imprévisible, car le code appelant placera sur la pile des types différents de ceux que la fonction attend. En C++ cela produira une erreur lors de l'édition de liens.

Lorsque le compilateur C++ analyse un appel de fonction, il recherche la fonction correspondante en observant les arguments qui lui sont passés (36).

S'il trouve une correspondance, il crée un CALL vers la fonction adéquate en utilisant les règles de décoration du compilateur.

Comme des compilateurs différents utilisent différentes règles de décoration de nom, il est possible que des codes C++ compilés par des compilateurs différents ne puissent pas être liés ensemble. C'est important lorsque l'on a l'intention d'utiliser une bibliothèque C++ précompilée ! Si l'on veut écrire une fonction en assembleur qui sera utilisée avec du code C++, il faut connaître les règles de décoration de nom du compilateur C++ utilisé (ou utiliser la technique expliquée plus bas).

L'étudiant astucieux pourrait se demander si le code de la Figure 7.10 fonctionnera de la façon attendue. Comme le C++ décore toutes les fonctions, alors la fonction printf sera décorée et le compilateur ne produira pas un CALL à l'étiquette _printf. C'est une question pertinente. Si le prototype de printf était simplement placé au début du fichier, cela arriverait. Son prototype est :

Sélectionnez

int printf ( const char , ...);

DJGPP décorerait ce nom en _printf__FPCce (F pour fonction, P pour pointeur, C pour const, c pour char et e pour ellipse). Cela n'appellerait pas la fonction printf de la bibliothèque C standard ! Bien sûr, il doit y avoir un moyen pour que le C++ puisse appeler du code C. C'est très important, car il existe une énorme quantité de vieux code C utile. En plus de permettre l'accès au code hérité de C, le C++ permet également d'appeler du code assembleur en utilisant les conventions de décoration standard du C.

Le C++ étend le mot-clé extern pour lui permettre de spécifier que la fonction ou la variable globale qu'il modifie utilise les conventions C normales.

Dans la terminologie C++, la fonction ou la variable globale utilise une édition de liens C. Par exemple, pour déclarer la fonction printf comme ayant une édition de liens C, utilisez le prototype :

Sélectionnez

extern "C" int printf ( const char , ... );

Cela impose au compilateur de ne pas utiliser les règles de décoration de nom du C++ sur la fonction, mais d'utiliser les règles C à la place. Cependant, en faisant cela, la fonction printf ne peut pas être surchargée. Cela constitue la façon la plus simple d'interfacer du C++ et de l'assembleur, définir une fonction comme utilisant une édition de liens C puis utiliser la convention d'appel C.

Pour plus de facilité, le C++ permet également de définir une édition de liens C sur un bloc de fonctions et de variables globales. Le bloc est indiqué en utilisant les accolades habituelles.

Sélectionnez

extern "C" {

/ variables globales et prototypes des fonctions ayant une édition de liens C /

}

Si l'on examine les fichiers d'en-tête ANSI C fournis avec les compilateurs C/C++ actuels, on trouve ce qui suit vers le début de chaque fichier d'entête :

Sélectionnez

#ifdef __cplusplus

extern "C" {

#endif

Et une construction similaire, vers la fin, contenant une accolade fermante.

Les compilateurs C++ définissent la macro __cplusplus (avec deux caractères de soulignement au début). L'extrait ci-dessus entoure tout le fichier d'en-tête dans un bloc extern "C" si le fichier d'en-tête est compilé en C++, mais ne fait rien s'il est compilé en C (puisqu'un compilateur C génèrerait une erreur de syntaxe sur extern "C"). La même technique peut être utilisée par n'importe quel programmeur pour créer un fichier d'en-tête pour des routines assembleur pouvant être utilisées en C ou en C++.

VIII-B-2. Références

Les références sont une autre nouvelle fonctionnalité du C++. Elles permettent de passer des paramètres à une fonction sans utiliser explicitement de pointeur. Par exemple, considérons le code de la Figure 7.11.

Image non disponible

En fait, les paramètres par référence sont plutôt simples, ce sont des pointeurs. Le compilateur masque simplement ce fait aux yeux du programmeur (exactement de la même façon que les compilateurs Pascal qui implémentent les paramètres var comme des pointeurs). Lorsque le compilateur génère l'assembleur pour l'appel de fonction ligne 7, il passe l'adresse de y. Si l'on écrivait la fonction f en assembleur, on ferait comme si le prototype était (37) :

Sélectionnez

void f ( int xp);

Les références sont juste une facilité qui est particulièrement utile pour la surcharge d'opérateurs. C'est une autre fonctionnalité du C++ qui permet de donner une signification aux opérateurs de base lorsqu'ils sont appliqués à des structures ou des classes. Par exemple, une utilisation courante est de définir l'opérateur plus (+) de façon à ce qu'il concatène les objets string.

Donc, si a et b sont des strings, a + b renverra la concaténation des chaînes a et b. Le C++ appellerait en réalité une fonction pour ce faire (en fait, cette expression pourrait être réécrite avec une notation fonction sous la forme operator +(a,b)). Pour plus d'efficacité, il est souhaitable de passer l'adresse des objets string à la place de les passer par valeur. Sans référence, cela pourrait être fait en écrivant operator +(&a,&b), mais cela imposerait d'écrire l'opérateur avec la syntaxe &a + &b. Cela serait très maladroit et confus. Par contre, en utilisant les références, il est possible d'écrire a + b, ce qui semble très naturel.

VIII-B-3. Fonctions inline

Les fonctions inline sont encore une autre fonctionnalité du C++ (38). Les fonctions inline sont destinées à remplacer les macros du préprocesseur qui prennent des paramètres, sources d'erreur. Sourvenez-vous en C, une macro qui élève un nombre au carré ressemble à cela :

Sélectionnez

#dene SQR(x) ((x)(x))

Comme le préprocesseur ne comprend pas le C et ne fait que de simples substitutions, les parenthèses sont requises pour calculer le résultat correct dans la plupart des cas. Cependant, même cette version ne donnerait pas la bonne réponse pour SQR(x++).

Les macros sont utilisées, car elles éliminent la surcharge d'un appel pour une fonction simple. Comme le chapitre sur les sous-programmes l'a démontré, effectuer un appel de fonction implique plusieurs étapes. Pour une fonction très simple, le temps passé à l'appeler peut être plus grand que celui passé dans la fonction ! Les fonctions inline sont une façon beaucoup plus pratique d'écrire du code qui ressemble à une fonction, mais qui n'effectue pas de CALL à un bloc commun. Au lieu de cela, les appels à des fonctions inline sont remplacés par le code de la fonction. Le C++ permet de rendre une fonction inline en plaçant le mot-clé inline au début de sa définition.

Par exemple, considérons les fonctions déclarées dans la Figure 7.12.

Image non disponible

L'appel à la fonction f, ligne 10, est un appel de fonction normal (en assembleur, en supposant que x est à l'adresse ebp-8 et y en ebp-4) :

Sélectionnez

1 push dword [ebp-8]

2 call _f

3 pop ecx

4 mov [ebp-4], eax

Cependant, l'appel à la fonction inline_f, ligne 11 ressemblerait à :

Sélectionnez

1 mov eax, [ebp-8]

2 imul eax, eax

3 mov [ebp-4], eax

Dans ce cas, il y a deux avantages à inliner. Tout d'abord, la fonction inline est plus rapide. Aucun paramètre n'est placé sur la pile, aucun cadre de pile n'est créé puis détruit, aucun branchement n'est effectué. Ensuite, l'appel à la fonction inline utilise moins de code ! Ce dernier point est vrai pour cet exemple, mais ne reste pas vrai dans tous les cas.

La principal inconvénient de l'inlining est que le code inline n'est pas lié et donc le code d'une fonction inline doit être disponible pour tous les fichiers qui l'utilisent. L'exemple de code assembleur précédent le montre. L'appel de la fonction non-inline ne nécessite que la connaissance des paramètres, du type de valeur de retour, de la convention d'appel et du nom de l'étiquette de la fonction. Toutes ces informations sont disponibles par le biais du prototype de la fonction. Cependant, l'utilisation de la fonction inline nécessite la connaissance de tout le code de la fonction. Cela signifie que si n'importe quelle partie de la fonction change, tous les fichiers source qui utilisent la fonction doivent être recompilés. Souvenez-vous que pour les fonctions non inline, si le prototype ne change pas, souvent les fichiers qui utilisent la fonction n'ont pas besoin d'être recompilés. Pour toutes ces raisons, le code des fonctions inline est généralement placé dans les fichiers d'en-tête. Cette pratique est contraire à la règle stricte habituelle du C selon laquelle on ne doit jamais placer de code exécutable dans les fichiers d'en-tête.

VIII-B-4. Classes

Une classe C++ décrit un type d'objet. Un objet possède à la fois des membres données et des membres fonctions (39). En d'autres termes, il s'agit d'une struct à laquelle sont associées des données et des fonctions. Considérons la classe simple dénie dans la Figure 7.13.

Image non disponible

Une variable de type Simple ressemblerait à une struct C normale avec un seul membre int.

Les fonctions ne sont pas affectées à la structure en mémoire. Cependant, les fonctions membres sont différentes des autres fonctions. On leur passe un paramètre caché. Ce paramètre est un pointeur vers l'objet sur lequel agit la fonction.

En fait, le C++ utilise le mot-clé this pour accéder au pointeur vers l'objet lorsque l'on se trouve à l'intérieur d'une fonction membre.

Par exemple, considérons la méthode set_data de la classe Simple de la Figure 7.13. Si elle était écrite en C, elle ressemblerait à une fonction à laquelle on passerait explicitement un pointeur vers l'objet sur lequel elle agit comme le montre le code de la Figure 7.14.

Image non disponible

L'option -S du compilateur DJGPP (et des compilateurs gcc et Borland également) indique au compilateur de produire un fichier assembleur contenant l'équivalent assembleur du code. Pour DJGPP et gcc le fichier assembleur possède une extension .s et utilise malheureusement la syntaxe du langage assembleur AT&T qui est assez différente des syntaxes NASM et MASM (40) (les compilateurs Borland et MS génèrent un fichier avec l'extension .asm utilisant la syntaxe MASM). La Figure 7.15 montre la sortie de DJGPP convertie en syntaxe NASM avec des commentaires supplémentaires pour clarifier le but des instructions.

Image non disponible

Sur la toute première ligne, notez que la méthode set_data reçoit une étiquette décorée qui encode le nom de la méthode, le nom de la classe et les paramètres.

Le nom de la classe est encodé, car d'autres classes peuvent avoir une méthode appelée set_data et les deux méthodes doivent recevoir des étiquettes différentes. Les paramètres sont encodés afin que la classe puisse surcharger la méthode set_data afin qu'elle prenne d'autres paramètres, comme les fonctions C++ normales. Cependant, comme précédemment, des compilateurs différents encoderont ces informations différemment dans l'étiquette.

Ensuite, aux lignes 2 et 3 nous retrouvons le prologue habituel. À la ligne 5, le premier paramètre sur la pile est stocké dans EAX. Ce n'est pas le paramètre x ! Il s'agit du paramètre caché (41) qui pointe vers l'objet sur lequel on agit.

La ligne 6 stocke le paramètre x dans EDX et la ligne 7 stocke EDX dans le double-mot sur lequel pointe EAX. Il s'agit du membre data de l'objet Simple sur lequel on agit, qui, étant la seule donnée de la classe, est stocké au déplacement 0 de la structure Simple.

VIII-B-4-a. Exemple

Cette section utilise les idées de ce chapitre pour créer une classe C++ qui représente un entier non signé d'une taille arbitraire. Comme l'entier peut faire n'importe quelle taille, il sera stocké dans un tableau d'entiers non signés (doubles-mots). Il peut faire n'importe quelle taille en utilisant l'allocation dynamique. Les doubles-mots sont stockés dans l'ordre inverse (42) (i.e. le double-mot le moins significatif est au déplacement 0). La Figure 7.16 montre la définition de la classe Big_int (43).

Image non disponible

Fig. 7.16 Dénition de la classe Big_int

La taille d'un Big_int est mesurée par la taille du tableau d'unsigned utilisé pour stocker les données.

La donnée membre size_ de la classe est affectée au déplacement 0 et le membre number_ est affecté au déplacement 4.

Pour simplifier l'exemple, seuls les objets ayant des tableaux de la même taille peuvent être additionnés entre eux.

La classe a trois constructeurs : le premier (ligne 9) initialise l'instance de la classe en utilisant un entier non signé normal, le second (ligne 19) initalise l'instance en utilisant une chaîne qui contient une valeur hexadécimale. Le troisième constructeur (ligne 22) est le constructeur par copie.

Cette explication se concentre sur la façon dont fonctionnent les opérateurs d'addition et de soustraction, car c'est là que l'on utilise de l'assembleur.

La Figure 7.17 montre les parties du fichier d'en-tête relatives à ces opérateurs.

Image non disponible

Elles montrent comment les opérateurs sont paramétrés pour appeler des routines assembleur. Comme des compilateurs différents utilisent des règles de décoration radicalement différentes pour les fonctions opérateur, des fonctions opérateur inline sont utilisées pour initialiser les appels aux routines assembleur liées au format C. Cela les rend relativement simples à porter sur des compilateurs différents et est aussi rapide qu'un appel direct.

Cette technique élimine également le besoin de soulever une exception depuis l'assembleur !

Pourquoi l'assembleur n'est-il utilisé qu'ici ? Souvenez-vous que pour effectuer de l'arithmétique en précision multiple, la retenue doit être ajoutée au double mot significatif suivant. Le C++ (et le C) ne permet pas au programmeur d'accéder au drapeau de retenue du processeur. On ne pourrait effectuer l'addition qu'en recalculant indépendamment en C++ la valeur du drapeau de retenue et en l'ajoutant de façon conditionnelle au double-mot suivant. Il est beaucoup plus efficace d'écrire le code en assembleur à partir duquel on peut accéder au drapeau de retenue et utiliser l'instruction ADC qui ajoute automatiquement le drapeau de retenue.

Par concision, seule la routine assembleur add_big_ints sera expliquée ici. Voici le code de cette routine (contenu dans big_math.asm) :

big_math.asm

Sélectionnez

1 segment .text
2 global add_big_ints, sub_big_ints
3 ?fine size_offset 0
4 ?fine number_offset 4
5
6 ?fine EXIT_OK 0
7 ?fine EXIT_OVERFLOW 1
8 ?fine EXIT_SIZE_MISMATCH 2
9
10 ; Paramètres des routines add et sub
11 ?fine res ebp+8
12 ?fine op1 ebp+12
13 ?fine op2 ebp+16
14
15 add_big_ints:
16 push ebp
17 mov ebp, esp
18 push ebx
19 push esi
20 push edi
21 ;
22 ; initialise esi pour pointer vers op1
23 ; edi pour pointer vers op2
24 ; ebx pour pointer vers res
25 mov esi, [op1]
26 mov edi, [op2]
27 mov ebx, [res]
28 ;
29 ; s'assure que les 3 Big_int ont la même taille
30 ;
31 mov eax, [esi + size_offset]
32 cmp eax, [edi + size_offset]
33 jne sizes_not_equal ; op1.size_ != op2.size_
34 cmp eax, [ebx + size_offset]
35 jne sizes_not_equal ; op1.size_ != res.size_
36
37 mov ecx, eax ; ecx = taille des Big_int
38 ;
39 ; initialise les registres pour qu'ils pointent vers leurs tableaux respectifs
40 ; esi = op1.number_
41 ; edi = op2.number_
42 ; ebx = res.number_
43 ;
44 mov ebx, [ebx + number_offset]
45 mov esi, [esi + number_offset]
46 mov edi, [edi + number_offset]
47
48 clc ; met le drapeau de retenue à 0
49 xor edx, edx ; edx = 0
50 ;
51 ; boucle d'addition
52 add_loop:
53 mov eax, [edi+4*edx]
54 adc eax, [esi+4*edx]
55 mov [ebx + 4*edx], eax
56 inc edx ; ne modifie pas le drapeau de retenue
57 loop add_loop
58
59 jc overflow
60 ok_done:
61 xor eax, eax ; valeur de retour = EXIT_OK
62 jmp done
63 overflow:
64 mov eax, EXIT_OVERFLOW
65 jmp done
66 sizes_not_equal:
67 mov eax, EXIT_SIZE_MISMATCH
68 done:
69 pop edi
70 pop esi
71 pop ebx
72 leave
73 ret

Heureusement, la majorité de ce code devrait être compréhensible pour le lecteur maintenant. Les lignes 25 à 27 stockent les pointeurs vers les objets Big_int passés à la fonction via des registres. Souvenez-vous que les références sont des pointeurs. Les lignes 31 à 35 vérifient que les tailles des trois objets sont les mêmes (notez que le déplacement de size_ est ajouté au pointeur pour accéder à la donnée membre). Les lignes 44 à 46 ajustent les registres pour qu'ils pointent vers les tableaux utilisés par leurs objets respectifs au lieu des objets eux-mêmes (là encore, le déplacement du membre number_ est ajouté au pointeur sur l'objet).

La boucle des lignes 52 à 57 additionne les entiers stockés dans les tableaux en additionnant le double-mot le moins significatif en premier, puis les doubles-mots suivants, etc. L'addition doit être effectuée dans cet ordre pour l'arithmétique en précision étendue (voir Section III.A.5).

La ligne 59 vérifie qu'il n'y a pas de dépassement de capacité, lors d'un dépassement de capacité, le drapeau de retenue sera allumé par la dernière addition du double-mot le plus significatif. Comme les doubles-mots du tableau sont stockés dans l'ordre little endian, la boucle commence au début du tableau et avance jusqu'à la fin.

La Figure 7.18 montre un court exemple utilisant la classe Big_int.

Image non disponible

Notez que les constantes de Big_int doivent être déclarées explicitement comme à la ligne ProductID="16. C" w:st="on">16. C'est nécessaire pour deux raisons. Tout d'abord, il n'y a pas de constructeur de conversion qui convertisse un entier non signé en Big_int. Ensuite, seuls les Big_int de même taille peuvent être additionnés. Cela rend la conversion problématique puisqu'il serait difficile de savoir vers quelle taille convertir. Une implémentation plus sophistiquée de la classe permettrait d'additionner n'importe quelle taille avec n'importe quelle autre. L'auteur ne voulait pas compliquer inutilement cet exemple en l'implémentant ici (cependant, le lecteur est encouragé à le faire).

VIII-B-5. Héritage et polymorphisme

L'héritage permet à une classe d'hériter des données et des méthodes d'une autre. Par exemple, considérons le code de la Figure 7.19.

Image non disponible

Il montre deux classes, A et B, où la classe B hérite de A. La sortie du programme est :

Sélectionnez

Taille de a : 4 Déplacement de ad : 0

Taille de b : 8 Déplacement de ad: 0 Déplacement de bd: 4

A::m()

A::m()

Notez que les membres ad des deux classes (B l'hérite de A) sont au même déplacement. C'est important puisque l'on peut passer à la fonction f soit un pointeur vers un objet A soit un pointeur vers un objet de n'importe quel type dérivé de (i.e. qui hérite de) A. La Figure 7.20 montre le code assembleur (édité) de la fonction (généré par gcc).

Image non disponible

Notez que la sortie de la méthode m de A a été produite à la fois par l'objet a et l'objet b. D'après l'assembleur, on peut voir que l'appel à A::m() est codé en dur dans la fonction. Dans le cadre d'une vraie programmation orientée objet, la méthode appelée devrait dépendre du type d'objet passé à la fonction. On appelle cela le polymorphisme. Le C++ désactive cette fonctionnalité par défaut. On utilise le mot-clé virtual pour l'activer. La Figure 7.21 montre comment les deux classes seraient modifiées.

Image non disponible

Rien dans le restet du code n'a besoin d'être changé. Le polymorphisme peut être implémenté de beaucoup de manières. Malheureusement, l'implémentation de gcc est en transition au moment d'écrire ces lignes et devient beaucoup plus compliquée que l'implémentation initiale. Afin de simplifier cette explication, l'auteur ne couvrira que l'implémentation du polymorphisme que les compilateurs Microsoft et Borland utilisent. Cette implémentation n'a pas changé depuis des années et ne changera probablement pas dans un futur proche.

Avec ces changements, la sortie du programme change :

Sélectionnez

Size of a: 8 Déplacement de ad : 4

Size of b: 12 Déplacement de ad : 4 Déplacement de bd : 8

A::m()

B::m()

Maintenant, le second appel à f appelle la méthode B::m(), car on lui passe un objet B. Ce n'est pas le seul changement cependant. La taille d'un A vaut maintenant 8 (et 12 pour B). De plus, le déplacement de ad vaut maintenant 4, plus 0. Qu'y a-t-il au déplacement 0 ? La réponse à cette question est liée à la façon dont est implémenté le polymorphisme.

Une classe C++ qui a une (ou plusieurs) méthode(s) virtuelle(s) a un champ caché qui est un pointeur vers un tableau de pointeurs sur des méthodes(44). Cette table est souvent appelée la vtable. Pour les classes A et B ce pointeur est stocké au déplacement 0. Les compilateurs Windows placent toujours ce pointeur au début de la classe au sommet de l'arbre d'héritage.

En regardant le code assembleur (Figure 7.22) généré pour la fonction f (de la Figure 7.19) dans la version du programme avec les méthodes virtuelles, on peut voir que l'appel à la méthode m ne se fait pas via une étiquette.

Image non disponible

La ligne 9 trouve l'adresse de la vtable de l'objet. L'adresse de l'objet est placée sur la pile ligne 11. La ligne 12 appelle la méthode virtuelle en se branchant à la première adresse dans la vtable (45). Cet appel n'utilise pas d'étiquette, il se branche à l'adresse du code sur lequel pointe EDX. Ce type d'appel est un exemple de liaison tardive (late binding). La liaison tardive repousse le choix de la méthode à appeler au moment de l'exécution du code. Cela permet d'appeler la méthode correspondant à l'objet. Le cas normal (Figure 7.20) code en dur un appel à une certaine méthode et est appelé liaison précoce (early binding), car la méthode est liée au moment de la compilation.

Le lecteur attentif se demandera pourquoi les méthodes de classe de la Figure 7.21 sont explicitement déclarées pour utiliser la convention d'appel C en utilisant le mot-clé __cdecl. Par défaut, Microsoft utilise une convention différente de la convention C standard pour les méthodes de classe C++. Il passe le pointeur sur l'objet sur lequel agit la méthode via le registre ECX au lieu d'utiliser la pile. La pile est toujours utilisée pour les autres paramètres explicites de la méthode. Le modificateur __cdecl demande l'utilisation de la convention d'appel C standard. Borland C++ utilise la convention d'appel C par défaut.

Observons maintenant un exemple légèrement plus compliqué (Figure 7.23).

Image non disponible

Dans celui-là, les classes A et B ont chacune deux méthodes : m1 et m2. Souvenez-vous que comme la classe B ne définit pas sa propre méthode m2, elle hérite de la méthode de classe de A. La Figure 7.24 montre comment l'objet b apparaît en mémoire.

Image non disponible

La Figure 7.25 montre la sortie du programme.

Image non disponible

Tout d'abord, regardons l'adresse de la vtable de chaque objet. Les adresses des deux objets B sont les mêmes, ils partagent donc la même vtable. Une vtable appartient à une classe pas à un objet (comme une donnée membre static). Ensuite, regardons les adresses dans les vtables. En regardant la sortie assembleur, on peut déterminer que le pointeur sur la méthode m1 est au déplacement 0 (ou dword 0) et celui sur m2 est au déplacement 4 (dword 1). Les pointeurs sur la méthode m2 sont les mêmes pour les vtables des classes A et B, car la classe B hérite de la méthode m2 de la classe A.

Les lignes 25 à 32 montrent comment l'on pourrait appeler une fonction virtuelle en lisant son adresse depuis la vtable de l'objet (46). L'adresse de la méthode est stockée dans un pointeur de fonction de type C avec un pointeur this explicite. D'après la sortie de la Figure 7.25, on peut voir comment cela fonctionne. Cependant, s'il vous plaît, n'écrivez pas de code de ce genre ! Il n'est utilisé que pour illustrer le fait que les méthodes virtuelles utilisent la vtable.

Il y a plusieurs leçons pratiques à tirer de cela. Un fait important est qu'il faut être très attentif lorsque l'on écrit ou qu'on lit des variables de type classe depuis un fichier binaire. On ne peut pas utiliser simplement une lecture ou une écriture binaire, car cela lirait ou écrirait le pointeur vtable depuis le fichier ! C'est un pointeur sur l'endroit où la vtable réside dans la mémoire du programme et il varie d'un programme à l'autre. La même chose peut arriver avec les structures en C, mais en C, les structures n'ont des pointeurs que si le programmeur en définit explicitement. Il n'y a pas de pointeurs explicites déclarés dans les classes A ou B.

Une fois encore, il est nécessaire de réaliser que des compilateurs différents implémentent les méthodes virtuelles différemment. Sous Windows, les objets de la classe COM (Component Object Model) utilisent des vtables pour implémenter les interfaces COM (47). Seuls les compilateurs qui implémentent les vtables des méthodes virtuelles comme le fait Microsoft peuvent créer des classes COM. C'est pourquoi Borland utilise la même implémentation que Microsoft et une des raisons pour lesquelles gcc ne peut pas être utilisé pour créer des classes COM.

Le code des méthodes virtuelles est identique à celui des méthodes non virtuelles.

Seul le code d'appel est différent. Si le compilateur peut être absolument sûr de la méthode virtuelle qui sera appelée, il peut ignorer la vtable et appeler la méthode directement (p.e., en utilisant la liaison précoce).

VIII-B-6. Autres fonctionnalités C++

Le fonctionnement des autres fonctionnalités du C++ (p.e., le RunTime Type Information, la gestion des exceptions et l'héritage multiple) dépasse le cadre de ce livre. Si le lecteur veut aller plus loin, The Annotated C++ Reference Manual de Ellis et Stroustrup et The Design and Evolution of C++ de Stroustrup constituent un bon point de départ.

IX. Annexe A : Instructions 80x86

IX-A. Instructions hors virgule flottante

Cette section liste et décrit les actions et les formats des instructions hors virgule flottante de la famille de processeurs Intel 80x86.

Les formats utilisent les abréviations suivantes :

Image non disponible

Elles peuvent être combinées pour les instructions à plusieurs opérandes. Par exemple, le format R,R signifie que l'instruction prend deux opérandes de type registre. Beaucoup d'instructions à deux opérandes acceptent les mêmes opérandes. L'abréviation O2 est utilisée pour représenter ces opérandes : R,R R,M R,I M,R M,I. Si un registre 8 bits ou un octet en mémoire peuvent être utilisés comme opérande, l'abréviation R/M8 est utilisée.

Le tableau montre également comment les différents bits du registre FLAGS sont modifiés par chaque instruction. Si la colonne est vide, le bit correspondant n'est pas modifié du tout. Si le bit est toujours positionné à une valeur particulière, un 1 ou un 0 figure dans la colonne. Si le bit est positionné à une valeur qui dépend des opérandes de l'instruction, un C figure dans la colonne. Enfin, si le bit est modifié de façon indéfinie, un « ? » figure dans la colonne. Comme les seules instructions qui changent le drapeau de direction sont CLD et STD, il ne figure pas parmi les colonnes FLAGS.

Image non disponible Image non disponible Image non disponible Image non disponible Image non disponible Image non disponible

IX-B. Instruction en virgule flottante

Dans cette section, la plupart des instructions du coprocesseur mathématique du 80x86 sont décrites. La section description décrit brièvement l'opération effectuée par l'instruction. Pour économiser de la place, il n'est pas précisé si l'instruction décale la pile ou non.

La colonne format indique le type d'opérande pouvant être utilisé avec chaque instruction. Les abréviations suivantes sont utilisées :

Image non disponible

Les instructions nécessitant un Pentium Pro ou supérieur sont signalées par un astérisque (*).

Image non disponible Image non disponible

(1)

L'indice 2 est utilisé pour indiquer que le nombre est représenté en binaire, pas en
décimal.

(2)

S'il n'est pas clair que le point de départ est important, essayez de convertir l'exemple en partant de la gauche.

(3)

En fait, l'ASCII n'utilise que les 7 bits les plus faibles et donc n'a que 128 valeurs à utiliser.

(4)

Cependant, ils peuvent également utiliser une interface de plus bas niveau au niveau du noyau.

(5)

Les nombres à virgule flottante en simple précision sont équivalents aux variables float en C.

(6)

TWORD définit une zone mémoire de 10 octets. Le coprocesseur virgule flottante utilise ce type de données.

(7)

Le fichier asm_io.inc (et le fichier objet asm_io que requiert asm_io.inc) sont dans les exemples de code à télécharger sur la page web de ce tutoriel, http://www.drpaulcarter.com/pcasm.

(8)

Le Chapitre III parle de ce registre.

(9)

GNU est un projet de Free Software Foundation (http://www.fsf.org).

(10)

http://www.delorie.com/djgpp.

(11)

Il est courant de penser que les fichiers ont un caractère EOF comme dernier caractère. Ce n'est pas vrai !

(12)

La raison de cette nécessité sera exposée plus tard.

(13)

2 est le seul nombre premier pair.

(14)

Cet opérateur est différent des opérateurs && binaire et & unaire !

(15)

Application Programming Interface, Interface pour la Programmation d'Applications

(16)

Signie Portable Operating System Interface for Computer Environments, Interface Portable de Système d'Exploitation pour les Environnements Informatiques. Un standard développé par l'IEEE basé sur UNIX.

(17)

En fait, un paramètre de type mode_t qui est défini comme entier par un typedef.

(18)

En fait, passer d'une représentation à l'autre pour entier consiste à inverser l'ordre des octets ; donc, convertir de little vers big ou de big vers little est la même opération. Donc, ces deux fonctions font la même chose.

(19)

En fait, il est également possible d'empiler des mots, mais en mode protégé 32 bits, il est mieux de ne travailler qu'avec des doubles-mots sur la pile.

(20)

Il est autorisé d'ajouter une constante à un registre lors de l'utilisation de l'adressage indirect. Des expressions plus complexes sont également possibles. Ce sujet est traité dans le chapitre suivant.

(21)

GAS est l'assembleur GNU que tous les compilateurs GNU utilisent. Il utilise la syntaxe AT&T qui est très différente des syntaxes relativement similaires de MASM, TASM et NASM.

(22)

Le compiltateur C Watcom est un exemple qui n'utilise pas les conventions standard par défaut. Voyez les sources exemple pour Watcom pour plus de détails.

(23)

Un programme multithreadé a plusieurs threads d'exécution. C'est-à-dire que le programme lui-même est multitâche.

(24)

Positionner AH à zéro équivaut à supposer implicitement que AL est un nombre non signé. S'il est signé, l'action appropriée serait d'insérer une instruction CBW entre les lignes 6 et 7.

(25)

On peut indiquer une taille, mais elle n'est pas prise en compte par le compilateur.

(26)

Une autre complication est qu'on ne peut pas copier la valeur du registre DS dans ES directement en utilisant une instruction MOV. À la place, la valeur de DS doit être copiée dans un registre universel (comme AX) puis être copié depuis ce registre dans ES, ce qui utilise deux instructions MOV.

(27)

Un préfixe d'instruction n'est pas une instruction, c'est un octet spécial qui est placé avant une instruction de chaîne qui modifie son comportement. D'autres préfixes sont également utilisés pour modifier le segment par défaut des accès mémoire.

(28)

Le type long double de certains compilateurs (comme celui de Borland) utilise cette précision étendue. Par contre, d'autres compilateurs utilisent la double précision à la fois pour les double et long double (c'est autorisé par le C ANSI).

(29)

La seule différence est que pour l'infini et les valeurs indéfinies, l'exposant décalé vaut 7FF et non pas FF.

(30)

La racine d'une fonction est une valeur x telle que f(x) = 0.

(31)

Cependant, le 80486SX n'avait pas de coprocesseur intégré. Il y avait une puce 80487SX pour ces machines.

(32)

Voyez le chapitre sur la gestion de la mémoire virtuelle de n'importe quel livre sur les Systèmes d'Exploitation pour une explication de ce terme.

(33)

En fait, le standard ANSI/ISO C laisse une certaine liberté au compilateur sur la façon d'organiser les bits. Cependant, les compilateurs C courants (gcc, Microsoft et Borland) organisent les champs comme cela.

(34)

Small Computer Systems Interface, un standard de l'industrie pour les disques durs, etc.

(35)

Mélanger différents types de champs de bits conduit à un comportement très étrange ! Le lecteur est invité à tester.

(36)

La correspondance n'a pas à être exacte, le compilateur prendra en compte les correspondances trouvées en transtypant les arguments. Les règles de ce procédé sont en dehors de la portée de ce livre. Consultez un livre sur le C++ pour plus de détails.

(37)

Bien sûr, il faudrait déclarer la fonction avec une édition de liens en C, comme nous en a7vons parlé dans la Section IIX.B.1.

(38)

Les compilateurs supportent souvent cette fonctionnalité comme une extension du C ANSI.

(39)

Souvent appelés fonctions membres en C++ ou plus généralement méthodes.

(40)

Le compilateur gcc inclut son propre assembleur appelé gas. L'assembleur gas utilise la syntaxe AT&T et donc le compilateur produit un code au format gas. Il y a plusieurs sites sur le web qui expliquent les différences entre les formats INTEL et AT&T. Il existe également un programme gratuit appelé a2i (http://membres.multimania.fr/placr/a2i.html), qui passe du format AT&T au format NASM.

(41)

Comme d'habitude, rien n'est caché dans le code assembleur !

(42)

Pourquoi ? Car les opérations d'addition commenceront ainsi toujours par le début du tableau et avanceront.

(43)

Voyez le code source d'exemple pour obtenir le code complet de cet exemple. Le texte ne se réfèrera qu'à certaines parties du code.

(44)

Pour les classes sans méthode virtuelle, les compilateurs C++ rendent toujours la classe compatible avec une structure C normale qui aurait les mêmes données membres.

(45)

Bien sûr, la valeur est déjà dans le registre ECX. Elle y a été placée à la ligne 8 et la ligne 10 pourrait être supprimée et la ligne suivante changée de façon à empiler ECX. Le code n'est pas très efficace, car il a été généré en désactivant les optimisations du compilateur.

(46)

Souvenez-vous, ce code ne fonctionne qu'avec les compilateurs MS et Borland, pas gcc.

(47)

Les classes COM utilisent également la convention d'appel __stdcall, pas la convention C standard.


Sommaire

BONJOUR
Musique "Invention BWV784" J.S. BACH 1685-1750

Tutoriel sur x86

- Assembleur -